From efa770080e93673e90f19432d7d2e40448d63668 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Thu, 13 Jul 2023 19:45:27 -0400 Subject: [PATCH 01/22] [LOOP-4681] initial meals main screen UI; tab bar updates --- Loop.xcodeproj/project.pbxproj | 8 + .../meals.imageset/Contents.json | 16 ++ .../meals.imageset/meals.pdf | Bin 0 -> 2234 bytes .../presets-selected.imageset/Contents.json | 16 ++ .../presets-selected.pdf | Bin 0 -> 2876 bytes .../presets.colorset/Contents.json | 38 +++ .../presets.imageset/Contents.json | 16 ++ .../presets.imageset/presets.pdf | Bin 0 -> 3000 bytes Loop/Extensions/EditMode.swift | 19 ++ Loop/Extensions/UIAlertController.swift | 24 ++ Loop/Extensions/UIImage.swift | 4 + .../StatusTableViewController.swift | 249 ++++++++++++------ Loop/Views/MealsView.swift | 234 ++++++++++++++++ LoopUI/Extensions/UIColor.swift | 4 + 14 files changed, 543 insertions(+), 85 deletions(-) create mode 100644 Loop/DefaultAssets.xcassets/meals.imageset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/meals.imageset/meals.pdf create mode 100644 Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/presets-selected.imageset/presets-selected.pdf create mode 100644 Loop/DefaultAssets.xcassets/presets.colorset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/presets.imageset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/presets.imageset/presets.pdf create mode 100644 Loop/Extensions/EditMode.swift create mode 100644 Loop/Views/MealsView.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index d9e4871882..5eb0f7daf3 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1419606428D9550400BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; }; 1419606928D9554E00BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; }; 1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; }; + 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB7582A60BF2E0075748A /* EditMode.swift */; }; + 142CB75B2A60BFC30075748A /* MealsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB75A2A60BFC30075748A /* MealsView.swift */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; @@ -785,6 +787,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 142CB7582A60BF2E0075748A /* EditMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMode.swift; sourceTree = ""; }; + 142CB75A2A60BFC30075748A /* MealsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealsView.swift; sourceTree = ""; }; 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SmallStatusWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -2332,6 +2336,7 @@ A9CBE457248AB564008E7BA2 /* DoseStore+SimulatedCoreData.swift */, A9B996F127238705002DC09C /* DosingDecisionStore.swift */, A9CBE459248ACBE1008E7BA2 /* DosingDecisionStore+SimulatedCoreData.swift */, + 142CB7582A60BF2E0075748A /* EditMode.swift */, A9F703742489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift */, A9DCF2D525B0F3C500C89088 /* LoopUIColorPalette+Default.swift */, 89E267FE229267DF00A3F2AF /* Optional.swift */, @@ -2389,6 +2394,7 @@ 430D85881F44037000AF2D4F /* HUDViewTableViewCell.swift */, A91D2A3E26CF0FF80023B075 /* IconTitleSubtitleTableViewCell.swift */, C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */, + 142CB75A2A60BFC30075748A /* MealsView.swift */, 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */, 899433B723FE129700FA4BEA /* OverrideBadgeView.swift */, 89D6953D23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift */, @@ -3822,6 +3828,7 @@ 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */, C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */, B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, + 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */, C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */, @@ -3849,6 +3856,7 @@ 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */, 1DA649A9244126DA00F61E75 /* InAppModalAlertScheduler.swift in Sources */, 43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */, + 142CB75B2A60BFC30075748A /* MealsView.swift in Sources */, A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */, diff --git a/Loop/DefaultAssets.xcassets/meals.imageset/Contents.json b/Loop/DefaultAssets.xcassets/meals.imageset/Contents.json new file mode 100644 index 0000000000..514195af8b --- /dev/null +++ b/Loop/DefaultAssets.xcassets/meals.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "meals.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Loop/DefaultAssets.xcassets/meals.imageset/meals.pdf b/Loop/DefaultAssets.xcassets/meals.imageset/meals.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a477df6b919c46daca6e107d627791c473c1b299 GIT binary patch literal 2234 zcmZuz(QaEe41L#E=p{fqpt49xlmrw7)?^)qZCK{+t=NO-+@>BJ-_3T1u3x`H*}is? zIY6o$iz3fCl-R4=n|H4$6z9lCKmPHj^ZxZ~|K^PwHh1=?<4b(@)3CYUfARuwZo78- z?XY-ovzy_c)pD4B|JGl>zx->tbALMuv44C#EnZEl={r7s$AOC|iC;drY1{L~Zn<%+ zAy<+}o+PAFGH3tbI0aReT0J4GY0lMi3@x_C>@{eM8nY)2kxOCm$fzmS=$DRis6`q# zfW(Pa{n}9ql}J+b6e#7a2$n!4XW|P85yXv?g;L}S297ASA{6aB1}p04QK+>lM}R_H zOSL2iu6mwNnU8pF&lps;FAaCqhkd6ZOke)G$crbnDb66fm(zbIFVos96>@bBFJ)? z8ZoLRTb`*##MD+APPcQiMu9uUYODzo8s~;XQ8-tKlg#)oPJ#or6J@so-yx!cL$B;v z%Prj}hlUU&03++G)?4ktOhrr70(~W*HK-h11);tbl%&+iuo}uKH|UD0A~bK=OgC8p zwU9ug6<^HO&L;*e z!VOQ8B|XNOQ*f9n0aJUI6owuV5KXqkSI0Q$0oS$L1nF{AtRZcRC9zdzg2#mR?j}^x z3OjN@gD|1vn!Pw^`ECbZp4hEVxi-iNp-Lzf%zE22E-Chy+7dO~EWB5CLAl=?vIn)V zaJ#`4_XUp}+Nm8}$Yfm5et6YHaSrZXGV#r}XN_d{<>NPQ<9>BEW{xxDG;nOT90n1g zr>8lTCK&T+_U&df?sxvf@0eHk%zpd#uffl57x%*v_<6WnE#8h_{DkkQ8B3A|lwfqA^59l98r22I0s_T%afrFvs9_oqD9 z<7dRuv-)>HE>6-%CmF%rl<-Xada+-u$NR^jU$(1)3|nohC@eEi_i?km5U zAUu(eAySO8ClH3mQwaCzDP#}TDR94Ctk%QUnR`FnK#mu?@nLxBU##w49z>bV$8qnC ea2I&?e)V-g_5FVti|xMC!BfWW>gu~+-u(xIThJc> literal 0 HcmV?d00001 diff --git a/Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json b/Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json new file mode 100644 index 0000000000..92eafb4695 --- /dev/null +++ b/Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "presets-selected.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Loop/DefaultAssets.xcassets/presets-selected.imageset/presets-selected.pdf b/Loop/DefaultAssets.xcassets/presets-selected.imageset/presets-selected.pdf new file mode 100644 index 0000000000000000000000000000000000000000..df10a133c2c2ac795504c2abe28e5ad952379130 GIT binary patch literal 2876 zcmZuzO>Y}F5WVwP@M0h-5Q@WZ0zrVrZi=EU>MFejJ*exA<3g5NNh#9)`aZ6>Gb=l5 zz@XmF4rksx4(ICj=G`kYSt-(1@BjEyY5n@Oe)C3+ySw&Nxh1~(Y24kPK4}kd-K>tM z!?<}=tDEtkhhbcQ|5jhWU;Y}7>TgBHPLH|>0Lj4l~+rdnq`>=+vq1rxoEG|@I&MpMa6 zdy7W|W__gP)>D~NCZEDZK&}|?u+*%vf+6op z#wwe6Z^n>Mj}w_Qx>a<}C_})`q*N1F=$SiYL?%)6#p?w}jKODT6J=y{BHdMl#tzTL zi6|oPJg8pA%W!(RQ3_xNN=f82dE;ZK(P}EjW?K?g4ky93 zlR(uZL=h(1JBIQT#FLA|=4=U$Gg*uBiAU#+v$jCEviDMug5p%Od;}GlfsCH_1;zv| zP-b%W7_<;Bf)or9^*HOo5)t)`z-KYZlTDDOWDaIZ^aKusBgVkHu*vAm`owkj2yChk z{IFJ7l1s4ns`K4~B%7+zxmBI9+=@amKpN4-T8hr5MHn2EK^Z+O2{QL790j)GHbuG` zIQJRU^>YQGSmU!z=c4y6B^VO!T?!zg{WCdbQ;E(N+v95-U17C1$z#HjkqV=JoQ)Dyk>IIt4OK+lV$Gs_KFlmEz?Tm1` z=-D_l76(ka_YA>bC$Qbi-L78-?f&S_dRwZ?KzhxhV7N)ol}&=7Gt7$Ui7t??k=Btu zexr8kSJeicKE=!YQLk<{_hScs z9`7DDZ}(sH2bp{L5|*hzRc@vWgsNKxYd!CCz5k4F=~?_oAPZ0F!c$A|$Qw%H*PGL3yT8Bu^vhxVsI1QF zT#@!)r`SdZ!2Kbsr-Y!{u>efH0N#(rzstmVo+CC-~ICLf8=>dL;wH) literal 0 HcmV?d00001 diff --git a/Loop/DefaultAssets.xcassets/presets.colorset/Contents.json b/Loop/DefaultAssets.xcassets/presets.colorset/Contents.json new file mode 100644 index 0000000000..ec9cb985f4 --- /dev/null +++ b/Loop/DefaultAssets.xcassets/presets.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.902", + "green" : "0.745", + "red" : "0.365" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.902", + "green" : "0.745", + "red" : "0.365" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/DefaultAssets.xcassets/presets.imageset/Contents.json b/Loop/DefaultAssets.xcassets/presets.imageset/Contents.json new file mode 100644 index 0000000000..217a8827cd --- /dev/null +++ b/Loop/DefaultAssets.xcassets/presets.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "presets.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Loop/DefaultAssets.xcassets/presets.imageset/presets.pdf b/Loop/DefaultAssets.xcassets/presets.imageset/presets.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a861e6e81661c31b48f5417c60013b1d429a5ca GIT binary patch literal 3000 zcmeH}!EV$r5Qgu5in&y34^3h_u@kAPM7I@+eO<1k`xu5zGKIkxSI;G zNZjhf%KOEc8P9J|=3se#am*vl7>5L!uiqHJ$qAgEGE=YZUnW8xzc=+we+v@f%C74A z)|7WFJ~ux%m07(xgT-a|uj<$j#-qdyNXE%A&FGnkk|HUj29ZWuXi;QP-7#7>pLA6{ zJ?0UY*f6Y*zxn_D-vrO%3hGec8!ntM$LoJ+Uc+7U>!8qocA)IR_ z@Gy{OKG&gSgaXBg1%eUtlkuRUeIkQiF19)Q7rlmyn7WIQV+w=ZlLMMObH!fdn#38d z-v5&;cZE>n@x|h3NB$Rj;jy-ZgB&IruNN__^hJ`Z1XLuFf=ev!gJoK-CSs4TU7i&a zqoYBVh>*Xaw0JiXFR!=cLlI+m3W!vUq9R6-lO-a7zdg$qSN2 zpp&4n9`2b~IyUiLy1-pY4jD8l)J-*9l@pni9OIVEpOLl`DIh;1W zDEo5T+&q2tp*7bm0nLV8+h0&whV30j>E3GGe>Z^Ssx8aiC0b2gWa%8aD2J>Z7_P*W0!63wj;F8 K*}=i Void) { + self.init( + title: NSLocalizedString("Choose a Preset", comment: "The title of the alert controller used to select a preset"), + message: nil, + preferredStyle: .actionSheet + ) + + addAction(UIAlertAction(title: "Workout Preset", style: .default) { _ in + handler(.legacyWorkout) + }) + + addAction(UIAlertAction(title: "Pre-Meal Preset", style: .default) { _ in + handler(.preMeal) + }) + + addCancelAction() + } + /** Initializes an ActionSheet-styled controller for selecting a workout duration diff --git a/Loop/Extensions/UIImage.swift b/Loop/Extensions/UIImage.swift index 908f0c965a..315f90a5d5 100644 --- a/Loop/Extensions/UIImage.swift +++ b/Loop/Extensions/UIImage.swift @@ -30,6 +30,10 @@ extension UIImage { return suffix } + + static func presetsImage(selected: Bool) -> UIImage? { + return UIImage(named: selected ? "presets-selected" : "presets") + } static func preMealImage(selected: Bool) -> UIImage? { return UIImage(named: selected ? "Pre-Meal Selected" : "Pre-Meal") diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 0d83756dd5..a2dbf47484 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -271,35 +271,40 @@ final class StatusTableViewController: LoopChartsTableViewController { private func setupToolbarItems() { let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) let carbs = UIBarButtonItem(image: UIImage(named: "carbs"), style: .plain, target: self, action: #selector(userTappedAddCarbs)) - let preMeal = createPreMealButtonItem(selected: false, isEnabled: true) + let meals = UIBarButtonItem(image: UIImage(named: "meals"), style: .plain, target: self, action: #selector(presentMealsScreen)) let bolus = UIBarButtonItem(image: UIImage(named: "bolus"), style: .plain, target: self, action: #selector(presentBolusScreen)) - let workout = createWorkoutButtonItem(selected: false, isEnabled: true) + let presets = createPresetsButtonItem() let settings = UIBarButtonItem(image: UIImage(named: "settings"), style: .plain, target: self, action: #selector(onSettingsTapped)) toolbarItems = [ carbs, space, - preMeal, + meals, space, bolus, space, - workout, + presets, space, settings ] } - + private func updateToolbarItems() { let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true - toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") + toolbarItems![0].accessibilityLabel = NSLocalizedString("Enter Carbs", comment: "The label of the carb entry button") toolbarItems![0].isEnabled = isPumpOnboarded toolbarItems![0].tintColor = UIColor.carbTintColor - toolbarItems![2].isEnabled = isPumpOnboarded && (automaticDosingStatus.automaticDosingEnabled || !FeatureFlags.simpleBolusCalculatorEnabled) + // TODO: - Fix localized string here + toolbarItems![0].accessibilityLabel = NSLocalizedString("Enter Carbs", comment: "The label of the carb entry button") + toolbarItems![2].isEnabled = isPumpOnboarded + toolbarItems![2].tintColor = UIColor.carbTintColor toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") toolbarItems![4].isEnabled = isPumpOnboarded toolbarItems![4].tintColor = UIColor.insulinTintColor + toolbarItems![6].accessibilityLabel = NSLocalizedString("Presets", comment: "The label of the presets button") toolbarItems![6].isEnabled = isPumpOnboarded + toolbarItems![6].tintColor = UIColor.presetTintColor toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") toolbarItems![8].tintColor = UIColor.secondaryLabel } @@ -523,7 +528,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } } - updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) +// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) if deviceManager.loopManager.settings.preMealTargetRange == nil { preMealMode = nil @@ -832,7 +837,11 @@ final class StatusTableViewController: LoopChartsTableViewController { guard oldValue != preMealMode else { return } - updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingStatus.automaticDosingEnabled) +// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingStatus.automaticDosingEnabled) + + toolbarItems![6] = createPresetsButtonItem() + +// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingStatus.automaticDosingEnabled) } } @@ -840,7 +849,6 @@ final class StatusTableViewController: LoopChartsTableViewController { let allowed = onboardingManager.isComplete && (automaticDosingEnabled || !FeatureFlags.simpleBolusCalculatorEnabled) && deviceManager.loopManager.settings.preMealTargetRange != nil - toolbarItems![2] = createPreMealButtonItem(selected: preMealMode ?? false && allowed, isEnabled: allowed) } private var workoutMode: Bool? = nil { @@ -848,13 +856,14 @@ final class StatusTableViewController: LoopChartsTableViewController { guard oldValue != workoutMode else { return } - - if let workoutMode = workoutMode { - let allowed = onboardingManager.isComplete - toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode, isEnabled: allowed) - } else { - toolbarItems![6].isEnabled = false - } +// if let workoutMode = workoutMode { +// let allowed = onboardingManager.isComplete +// toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode, isEnabled: allowed) +// } else { +// toolbarItems![6].isEnabled = false +// } + + toolbarItems![6] = createPresetsButtonItem() } } @@ -1382,6 +1391,14 @@ final class StatusTableViewController: LoopChartsTableViewController { @IBAction func presentBolusScreen() { presentBolusEntryView() } + + @objc func presentMealsScreen() { + let hostingController: DismissibleHostingController + + let mealsView = MealsView().environmentObject(deviceManager.displayGlucosePreference) + hostingController = DismissibleHostingController(rootView: mealsView, isModalInPresentation: false) + present(hostingController, animated: true) + } func presentBolusEntryView(enableManualGlucoseEntry: Bool = false) { let hostingController: DismissibleHostingController @@ -1403,10 +1420,13 @@ final class StatusTableViewController: LoopChartsTableViewController { present(navigationWrapper, animated: true) deviceManager.analyticsServicesManager.didDisplayBolusScreen() } - - private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(togglePreMealMode(_:))) - item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") + + private func createPresetsButtonItem() -> UIBarButtonItem { + let selected = workoutMode == true || preMealMode == true + + let item = UIBarButtonItem(image: UIImage.presetsImage(selected: selected)!, style: .plain, target: self, action: #selector(onPresetsTapped)) + // MARK: - Needs localization + item.accessibilityLabel = NSLocalizedString("Presets", comment: "The label of the presets mode toggle button") if selected { item.accessibilityTraits.insert(.selected) @@ -1415,93 +1435,152 @@ final class StatusTableViewController: LoopChartsTableViewController { item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") } - item.tintColor = UIColor.carbTintColor - item.isEnabled = isEnabled + item.tintColor = UIColor.presetTintColor return item } - private func createWorkoutButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .plain, target: self, action: #selector(toggleWorkoutMode(_:))) - item.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") - - if selected { - item.accessibilityTraits.insert(.selected) - item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") - } else { - item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") - } - - item.tintColor = UIColor.glucoseTintColor - item.isEnabled = isEnabled - - return item - } +// private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { +// let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(togglePreMealMode(_:))) +// item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") +// +// if selected { +// item.accessibilityTraits.insert(.selected) +// item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") +// } else { +// item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") +// } +// +// item.tintColor = UIColor.carbTintColor +// item.isEnabled = isEnabled +// +// return item +// } + +// private func createWorkoutButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { +// let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .plain, target: self, action: #selector(toggleWorkoutMode(_:))) +// item.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") +// +// if selected { +// item.accessibilityTraits.insert(.selected) +// item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") +// } else { +// item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") +// } +// +// item.tintColor = UIColor.glucoseTintColor +// item.isEnabled = isEnabled +// +// return item +// } - @IBAction func togglePreMealMode(_ sender: UIBarButtonItem) { + @objc func onPresetsTapped() { if preMealMode == true { deviceManager.loopManager.mutateSettings { settings in settings.clearOverride(matching: .preMeal) } - } else { - let vc = UIAlertController(premealDurationSelectionHandler: { duration in - let startDate = Date() - - guard self.workoutMode != true else { - // allow cell animation when switching between presets - self.deviceManager.loopManager.mutateSettings { settings in - settings.clearOverride() - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.deviceManager.loopManager.mutateSettings { settings in - settings.enablePreMealOverride(at: startDate, for: duration) - } - } - return - } - - self.deviceManager.loopManager.mutateSettings { settings in - settings.enablePreMealOverride(at: startDate, for: duration) - } - }) - - present(vc, animated: true, completion: nil) } - } - - @IBAction func toggleWorkoutMode(_ sender: UIBarButtonItem) { - if workoutMode == true { + else if workoutMode == true { deviceManager.loopManager.mutateSettings { settings in settings.clearOverride() } - } else { + } + else { if FeatureFlags.sensitivityOverridesEnabled { performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) } else { - let vc = UIAlertController(workoutDurationSelectionHandler: { duration in - let startDate = Date() + presentPresetAlertController() + } + } + } + + func presentPresetAlertController() { + let vc = UIAlertController(presetTypeSelectionHandler: { [self] presetType in + switch presetType { + case .preMeal: + self.presentPreMealModeAlertController() + case .legacyWorkout: + self.presentWorkoutModeAlertController() + default: + assertionFailure("Unknown preset selected from presetAlertController: \(presetType)") + } + }) - guard self.preMealMode != true else { - // allow cell animation when switching between presets - self.deviceManager.loopManager.mutateSettings { settings in - settings.clearOverride(matching: .preMeal) - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.deviceManager.loopManager.mutateSettings { settings in - settings.enableLegacyWorkoutOverride(at: startDate, for: duration) - } - } - return + present(vc, animated: true, completion: nil) + } + +// @IBAction func togglePreMealMode(_ sender: UIBarButtonItem) { +// if preMealMode == true { +// deviceManager.loopManager.mutateSettings { settings in +// settings.clearOverride(matching: .preMeal) +// } +// } else { +// presentPreMealModeAlertController() +// } +// } + + func presentPreMealModeAlertController() { + let vc = UIAlertController(premealDurationSelectionHandler: { duration in + let startDate = Date() + + guard self.workoutMode != true else { + // allow cell animation when switching between presets + self.deviceManager.loopManager.mutateSettings { settings in + settings.clearOverride() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.deviceManager.loopManager.mutateSettings { settings in + settings.enablePreMealOverride(at: startDate, for: duration) } + } + return + } + self.deviceManager.loopManager.mutateSettings { settings in + settings.enablePreMealOverride(at: startDate, for: duration) + } + }) + + present(vc, animated: true, completion: nil) + } + +// @IBAction func toggleWorkoutMode(_ sender: UIBarButtonItem) { +// if workoutMode == true { +// deviceManager.loopManager.mutateSettings { settings in +// settings.clearOverride() +// } +// } else { +// if FeatureFlags.sensitivityOverridesEnabled { +// performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) +// } else { +// presentWorkoutModeAlertController() +// } +// } +// } + + func presentWorkoutModeAlertController() { + let vc = UIAlertController(workoutDurationSelectionHandler: { duration in + let startDate = Date() + + guard self.preMealMode != true else { + // allow cell animation when switching between presets + self.deviceManager.loopManager.mutateSettings { settings in + settings.clearOverride(matching: .preMeal) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.deviceManager.loopManager.mutateSettings { settings in settings.enableLegacyWorkoutOverride(at: startDate, for: duration) } - }) + } + return + } - present(vc, animated: true, completion: nil) + self.deviceManager.loopManager.mutateSettings { settings in + settings.enableLegacyWorkoutOverride(at: startDate, for: duration) } - } + }) + + present(vc, animated: true, completion: nil) } @IBAction func onSettingsTapped(_ sender: UIBarButtonItem) { @@ -1596,7 +1675,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func automaticDosingStatusChanged(_ automaticDosingEnabled: Bool) { - updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) +// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) hudView?.loopCompletionHUD.loopIconClosed = automaticDosingEnabled hudView?.loopCompletionHUD.closedLoopDisallowedLocalizedDescription = deviceManager.closedLoopDisallowedLocalizedDescription } diff --git a/Loop/Views/MealsView.swift b/Loop/Views/MealsView.swift new file mode 100644 index 0000000000..e565574850 --- /dev/null +++ b/Loop/Views/MealsView.swift @@ -0,0 +1,234 @@ +// +// MealsView.swift +// Loop +// +// Created by Noah Brauner on 7/12/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import LoopKitUI +import HealthKit + +struct MealsView: View { + @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference + @Environment(\.dismissAction) private var dismiss + @Environment(\.carbTintColor) private var carbTintColor + + @State private var mealToConfirmDeleteId: String? = nil + @State private var editMode: EditMode = .inactive + + @State private var meals = allMeals + + @State var isBolusViewActive = false + @State var isEditViewActive = false + @State var isAddViewActive = false + + @State var selectedMeal: Meal? = nil + + @State private var draggingMeal: Meal? + @State private var hasChangedLocation: Bool = false + + var body: some View { + NavigationViewWrappedContent { + ScrollView { + VStack(spacing: 16) { + VStack(spacing: 10) { + HStack { + Text("All Meals") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + + Spacer() + + editButton + } + + ForEach(meals) { meal in + draggableMealCardView(meal: meal) + } + } + .environment(\.editMode, self.$editMode) + + newMealButton + } + .padding() + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + dismissButton + } + } + .navigationBarTitle(Text(NSLocalizedString("Meals", comment: "Meals screen title"))) + .navigationViewStyle(.stack) + + NavigationLink(destination: Text("Coming later: bolus screen for \(selectedMeal?.name ?? "")"), isActive: $isBolusViewActive) { + EmptyView() + } + + NavigationLink(destination: Text("Coming later: edit meal screen for \(selectedMeal?.name ?? "")"), isActive: $isEditViewActive) { + EmptyView() + } + + NavigationLink(destination: Text("Coming later: add meal screen"), isActive: $isAddViewActive) { + EmptyView() + } + } + .onChange(of: editMode) { newValue in + if !newValue.isEditing { + mealToConfirmDeleteId = nil + } + } + } + + private func addMeal() { + isAddViewActive = true + } + + private func onMealTap(_ meal: Meal) { + selectedMeal = meal + if editMode.isEditing { + isEditViewActive = true + } + else { + isBolusViewActive = true + } + } + private func onMealDelete(_ meal: Meal) { + withAnimation(.easeInOut(duration: 0.3)) { + _ = meals.remove(meal) + } + } + + private func onMealReorder(from: IndexSet, to: Int) { + withAnimation { + meals.move(fromOffsets: from, toOffset: to) + } + } +} + +extension MealsView { + @ViewBuilder func draggableMealCardView(meal: Meal) -> some View { + Button(action: { + onMealTap(meal) + }) { + MealCardView(meal: meal, mealToConfirmDeleteId: $mealToConfirmDeleteId, onMealTap: onMealTap(_:), onMealDelete: onMealDelete(_:)) + .onDrag { + draggingMeal = meal + return NSItemProvider(object: "\(meal.id)" as NSString) + } preview: { + MealCardView(meal: meal, mealToConfirmDeleteId: $mealToConfirmDeleteId, onMealTap: onMealTap(_:), onMealDelete: onMealDelete(_:)) + } + .onDrop( + of: [UTType.text], + delegate: DragRelocateDelegate( + item: meal, + listData: meals, + current: $draggingMeal, + hasChangedLocation: $hasChangedLocation + ) { from, to in + onMealReorder(from: from, to: to) + } + ) + .disabled(!editMode.isEditing) + .buttonStyle(ListButtonStyle()) + } + } + + private var dismissButton: some View { + Button(action: dismiss) { + Text("Cancel") + } + } + +// private var plusButton: some View { +// Button(action: addMeal) { +// Image(systemName: "plus") +// } +// } + + private var editButton: some View { + Button(action: { + withAnimation(.easeInOut(duration: 0.3)) { + editMode.toggle() + } + }) { + Text(editMode.title) + } + } + + private var newMealButton: some View { + Button(action: addMeal) { + HStack { + Image(systemName: "plus.circle.fill") + + Text("Add a new meal") + } + } + .buttonStyle(ActionButtonStyle()) + .padding(.top) + } +} + +fileprivate struct NavigationViewWrappedContent: View { + var content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + NavigationView { + ZStack { + Color(.systemGroupedBackground) + .edgesIgnoringSafeArea(.all) + + content + } + } + } +} + +fileprivate struct DragRelocateDelegate: DropDelegate { + let item: Item + var listData: [Item] + @Binding var current: Item? + @Binding var hasChangedLocation: Bool + + var moveAction: (IndexSet, Int) -> Void + + func dropEntered(info: DropInfo) { + guard item != current, let current = current else { return } + guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return } + + hasChangedLocation = true + + if listData[to] != current { + moveAction(IndexSet(integer: from), to > from ? to + 1 : to) + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + hasChangedLocation = false + current = nil + return true + } +} + +fileprivate let allMeals = [ + // Some really yummy foods... + Meal(carbsQuantity: carbs(55), foodType: "🥞🥚", absorptionTime: .hours(3), name: "Pancakes and Eggs"), + Meal(carbsQuantity: carbs(35), foodType: "🍌🍞", absorptionTime: .hours(2), name: "Banana Bread"), + Meal(carbsQuantity: carbs(63), foodType: "🍞🥜🍫🥛", absorptionTime: .hours(3), name: "The Best Lunch"), + Meal(carbsQuantity: carbs(120), foodType: "🍕", absorptionTime: .hours(5), name: "Dad's Pizza"), +] + +fileprivate func carbs(_ value: Double) -> HKQuantity { + return HKQuantity(unit: .gram(), doubleValue: value) +} diff --git a/LoopUI/Extensions/UIColor.swift b/LoopUI/Extensions/UIColor.swift index db638084f8..ba1c5700f8 100644 --- a/LoopUI/Extensions/UIColor.swift +++ b/LoopUI/Extensions/UIColor.swift @@ -18,6 +18,8 @@ extension UIColor { @nonobjc static let insulin = UIColor(named: "insulin") ?? systemOrange + @nonobjc static let preset = UIColor(named: "presets") ?? systemBlue + // The loopAccent color is intended to be use as the app accent color. @nonobjc public static let loopAccent = UIColor(named: "accent") ?? systemBlue @@ -50,6 +52,8 @@ extension UIColor { @nonobjc public static let insulinTintColor = insulin + @nonobjc public static let presetTintColor = preset + @nonobjc public static let pumpStatusNormal = insulin @nonobjc public static let staleColor = critical From b81f8895459ea886b07bec34b39ff8601c335e87 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Mon, 17 Jul 2023 13:46:07 -0700 Subject: [PATCH 02/22] [CPA-62] Remove trendRateUnit from Glucose Store --- Common/Models/StatusExtensionContext.swift | 5 ++--- Common/Models/WatchContext.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Common/Models/StatusExtensionContext.swift b/Common/Models/StatusExtensionContext.swift index d1bef90f76..0c6cbbce8a 100644 --- a/Common/Models/StatusExtensionContext.swift +++ b/Common/Models/StatusExtensionContext.swift @@ -160,8 +160,8 @@ extension GlucoseDisplayableContext: RawRepresentable { trendType = nil } - if let trendRateUnit = rawValue["trendRateUnit"] as? String, let trendRateValue = rawValue["trendRateValue"] as? Double { - trendRate = HKQuantity(unit: HKUnit(from: trendRateUnit), doubleValue: trendRateValue) + if let trendRateValue = rawValue["trendRateValue"] as? Double { + trendRate = HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: trendRateValue) } else { trendRate = nil } @@ -181,7 +181,6 @@ extension GlucoseDisplayableContext: RawRepresentable { ] raw["trendType"] = trendType?.rawValue if let trendRate = trendRate { - raw["trendRateUnit"] = HKUnit.milligramsPerDeciliterPerMinute.unitString raw["trendRateValue"] = trendRate.doubleValue(for: HKUnit.milligramsPerDeciliterPerMinute) } raw["glucoseRangeCategory"] = glucoseRangeCategory?.rawValue diff --git a/Common/Models/WatchContext.swift b/Common/Models/WatchContext.swift index e032d0ec8c..3ce3adebf1 100644 --- a/Common/Models/WatchContext.swift +++ b/Common/Models/WatchContext.swift @@ -75,8 +75,8 @@ final class WatchContext: RawRepresentable { if let rawGlucoseTrend = rawValue["gt"] as? GlucoseTrend.RawValue { glucoseTrend = GlucoseTrend(rawValue: rawGlucoseTrend) } - if let glucoseTrendRateUnitString = rawValue["gtru"] as? String, let glucoseTrendRateValue = rawValue["gtrv"] as? Double { - glucoseTrendRate = HKQuantity(unit: HKUnit(from: glucoseTrendRateUnitString), doubleValue: glucoseTrendRateValue) + if let glucoseTrendRateValue = rawValue["gtrv"] as? Double { + glucoseTrendRate = HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: glucoseTrendRateValue) } glucoseDate = rawValue["gd"] as? Date glucoseIsDisplayOnly = rawValue["gdo"] as? Bool From 8240def5f3c0b185b162657461b40f48fd025f86 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Wed, 19 Jul 2023 11:18:04 -0400 Subject: [PATCH 03/22] [LOOP-4685] secret favorites foods feature unlock --- .../Contents.json | 0 .../meals.pdf | Bin Loop/Managers/SettingsManager.swift | 4 +- .../CarbEntryViewController.swift | 10 + .../StatusTableViewController.swift | 235 +++++++++++------- Loop/Views/MealsView.swift | 12 +- LoopCore/LoopSettings.swift | 13 +- 7 files changed, 171 insertions(+), 103 deletions(-) rename Loop/DefaultAssets.xcassets/{meals.imageset => favorite-foods.imageset}/Contents.json (100%) rename Loop/DefaultAssets.xcassets/{meals.imageset => favorite-foods.imageset}/meals.pdf (100%) diff --git a/Loop/DefaultAssets.xcassets/meals.imageset/Contents.json b/Loop/DefaultAssets.xcassets/favorite-foods.imageset/Contents.json similarity index 100% rename from Loop/DefaultAssets.xcassets/meals.imageset/Contents.json rename to Loop/DefaultAssets.xcassets/favorite-foods.imageset/Contents.json diff --git a/Loop/DefaultAssets.xcassets/meals.imageset/meals.pdf b/Loop/DefaultAssets.xcassets/favorite-foods.imageset/meals.pdf similarity index 100% rename from Loop/DefaultAssets.xcassets/meals.imageset/meals.pdf rename to Loop/DefaultAssets.xcassets/favorite-foods.imageset/meals.pdf diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index f7421b97f7..e7ee416396 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -109,7 +109,8 @@ class SettingsManager { maximumBolus: latestSettings.maximumBolus, suspendThreshold: latestSettings.suspendThreshold, automaticDosingStrategy: latestSettings.automaticDosingStrategy, - defaultRapidActingModel: latestSettings.defaultRapidActingModel?.presetForRapidActingInsulin) + defaultRapidActingModel: latestSettings.defaultRapidActingModel?.presetForRapidActingInsulin, + favoriteFoodsEnabled: latestSettings.favoriteFoodsEnabled) } } @@ -132,6 +133,7 @@ class SettingsManager { deviceToken: deviceToken, insulinType: deviceStatusProvider?.pumpManagerStatus?.insulinType, defaultRapidActingModel: newLoopSettings.defaultRapidActingModel.map(StoredInsulinModel.init), + favoriteFoodsEnabled: newLoopSettings.favoriteFoodsEnabled, basalRateSchedule: newLoopSettings.basalRateSchedule, insulinSensitivitySchedule: newLoopSettings.insulinSensitivitySchedule, carbRatioSchedule: newLoopSettings.carbRatioSchedule, diff --git a/Loop/View Controllers/CarbEntryViewController.swift b/Loop/View Controllers/CarbEntryViewController.swift index 7a8c950a34..db5cdcea1c 100644 --- a/Loop/View Controllers/CarbEntryViewController.swift +++ b/Loop/View Controllers/CarbEntryViewController.swift @@ -173,6 +173,8 @@ final class CarbEntryViewController: LoopChartsTableViewController, Identifiable }() private var lastContentHeight: CGFloat = 0 + + public weak var favoriteFoodsDelegate: FavoriteFoodsFeatureUnlockDelegate? override func createChartsManager() -> ChartsManager { // Consider including a chart on this screen to demonstrate how absorption time affects prediction @@ -552,6 +554,10 @@ final class CarbEntryViewController: LoopChartsTableViewController, Identifiable guard let updatedEntry = updatedCarbEntry else { return } + + if foodType == "🍞🥜🍫🥛" && quantity?.doubleValue(for: .gram()) == 63 { + favoriteFoodsDelegate?.featureAvailabilityChanged() + } let viewModel = BolusEntryViewModel( delegate: deviceManager, @@ -784,3 +790,7 @@ extension CarbEntryViewController: EmojiInputControllerDelegate { extension DateAndDurationTableViewCell: NibLoadable {} extension DateAndDurationSteppableTableViewCell: NibLoadable {} + +protocol FavoriteFoodsFeatureUnlockDelegate: AnyObject { + func featureAvailabilityChanged() +} diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index a2dbf47484..7a1eb35707 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -271,22 +271,40 @@ final class StatusTableViewController: LoopChartsTableViewController { private func setupToolbarItems() { let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) let carbs = UIBarButtonItem(image: UIImage(named: "carbs"), style: .plain, target: self, action: #selector(userTappedAddCarbs)) - let meals = UIBarButtonItem(image: UIImage(named: "meals"), style: .plain, target: self, action: #selector(presentMealsScreen)) let bolus = UIBarButtonItem(image: UIImage(named: "bolus"), style: .plain, target: self, action: #selector(presentBolusScreen)) - let presets = createPresetsButtonItem() let settings = UIBarButtonItem(image: UIImage(named: "settings"), style: .plain, target: self, action: #selector(onSettingsTapped)) - toolbarItems = [ - carbs, - space, - meals, - space, - bolus, - space, - presets, - space, - settings - ] + let favoriteFoodsEnabled = deviceManager.loopManager.settings.favoriteFoodsEnabled + if favoriteFoodsEnabled { + let meals = UIBarButtonItem(image: UIImage(named: "meals"), style: .plain, target: self, action: #selector(presentMealsScreen)) + let presets = createPresetsButtonItem(selected: false, isEnabled: true) + toolbarItems = [ + carbs, + space, + meals, + space, + bolus, + space, + presets, + space, + settings + ] + } + else { + let preMeal = createPreMealButtonItem(selected: false, isEnabled: true) + let workout = createWorkoutButtonItem(selected: false, isEnabled: true) + toolbarItems = [ + carbs, + space, + preMeal, + space, + bolus, + space, + workout, + space, + settings + ] + } } private func updateToolbarItems() { @@ -295,18 +313,23 @@ final class StatusTableViewController: LoopChartsTableViewController { toolbarItems![0].accessibilityLabel = NSLocalizedString("Enter Carbs", comment: "The label of the carb entry button") toolbarItems![0].isEnabled = isPumpOnboarded toolbarItems![0].tintColor = UIColor.carbTintColor - // TODO: - Fix localized string here - toolbarItems![0].accessibilityLabel = NSLocalizedString("Enter Carbs", comment: "The label of the carb entry button") - toolbarItems![2].isEnabled = isPumpOnboarded - toolbarItems![2].tintColor = UIColor.carbTintColor toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") toolbarItems![4].isEnabled = isPumpOnboarded toolbarItems![4].tintColor = UIColor.insulinTintColor - toolbarItems![6].accessibilityLabel = NSLocalizedString("Presets", comment: "The label of the presets button") - toolbarItems![6].isEnabled = isPumpOnboarded - toolbarItems![6].tintColor = UIColor.presetTintColor toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") toolbarItems![8].tintColor = UIColor.secondaryLabel + + let favoriteFoodsEnabled = deviceManager.loopManager.settings.favoriteFoodsEnabled + if favoriteFoodsEnabled { + toolbarItems![2] = createFavoriteFoodsButtonItem() + + let selected = (preMealMode == true && preMealModeAllowed) || (workoutMode == true && workoutModeAllowed) + toolbarItems![6] = createPresetsButtonItem(selected: selected, isEnabled: preMealModeAllowed || workoutModeAllowed) + } + else { + toolbarItems![2] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) + toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) + } } public var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil { @@ -837,18 +860,20 @@ final class StatusTableViewController: LoopChartsTableViewController { guard oldValue != preMealMode else { return } -// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingStatus.automaticDosingEnabled) - - toolbarItems![6] = createPresetsButtonItem() - -// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingStatus.automaticDosingEnabled) + updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingStatus.automaticDosingEnabled) } } + private lazy var preMealModeAllowed: Bool = { + onboardingManager.isComplete && + (automaticDosingStatus.automaticDosingEnabled || !FeatureFlags.simpleBolusCalculatorEnabled) + && deviceManager.loopManager.settings.preMealTargetRange != nil + }() private func updatePreMealModeAvailability(automaticDosingEnabled: Bool) { - let allowed = onboardingManager.isComplete && + preMealModeAllowed = onboardingManager.isComplete && (automaticDosingEnabled || !FeatureFlags.simpleBolusCalculatorEnabled) && deviceManager.loopManager.settings.preMealTargetRange != nil + updateToolbarItems() } private var workoutMode: Bool? = nil { @@ -856,16 +881,13 @@ final class StatusTableViewController: LoopChartsTableViewController { guard oldValue != workoutMode else { return } -// if let workoutMode = workoutMode { -// let allowed = onboardingManager.isComplete -// toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode, isEnabled: allowed) -// } else { -// toolbarItems![6].isEnabled = false -// } - - toolbarItems![6] = createPresetsButtonItem() + workoutModeAllowed = workoutMode != nil && onboardingManager.isComplete + updateToolbarItems() } } + private lazy var workoutModeAllowed: Bool = { + workoutMode != nil && onboardingManager.isComplete + }() // MARK: - Table view data source @@ -1375,7 +1397,7 @@ final class StatusTableViewController: LoopChartsTableViewController { hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) } else { let carbEntryViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "CarbEntryViewController") as! CarbEntryViewController - + carbEntryViewController.favoriteFoodsDelegate = self carbEntryViewController.deviceManager = deviceManager carbEntryViewController.defaultAbsorptionTimes = deviceManager.carbStore.defaultAbsorptionTimes carbEntryViewController.preferredCarbUnit = deviceManager.carbStore.preferredUnit @@ -1421,11 +1443,17 @@ final class StatusTableViewController: LoopChartsTableViewController { deviceManager.analyticsServicesManager.didDisplayBolusScreen() } - private func createPresetsButtonItem() -> UIBarButtonItem { - let selected = workoutMode == true || preMealMode == true - + private func createFavoriteFoodsButtonItem() -> UIBarButtonItem { + let item = UIBarButtonItem(image: UIImage(named: "favorite-foods")!, style: .plain, target: self, action: #selector(presentMealsScreen)) + item.accessibilityLabel = NSLocalizedString("Favorite Foods", comment: "The label of the favorite foods button") + + item.tintColor = UIColor.carbTintColor + + return item + } + + private func createPresetsButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { let item = UIBarButtonItem(image: UIImage.presetsImage(selected: selected)!, style: .plain, target: self, action: #selector(onPresetsTapped)) - // MARK: - Needs localization item.accessibilityLabel = NSLocalizedString("Presets", comment: "The label of the presets mode toggle button") if selected { @@ -1436,43 +1464,44 @@ final class StatusTableViewController: LoopChartsTableViewController { } item.tintColor = UIColor.presetTintColor + item.isEnabled = isEnabled return item } -// private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { -// let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(togglePreMealMode(_:))) -// item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") -// -// if selected { -// item.accessibilityTraits.insert(.selected) -// item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") -// } else { -// item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") -// } -// -// item.tintColor = UIColor.carbTintColor -// item.isEnabled = isEnabled -// -// return item -// } + private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { + let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(togglePreMealMode(_:))) + item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") + + if selected { + item.accessibilityTraits.insert(.selected) + item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") + } else { + item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") + } + + item.tintColor = UIColor.carbTintColor + item.isEnabled = isEnabled + + return item + } -// private func createWorkoutButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { -// let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .plain, target: self, action: #selector(toggleWorkoutMode(_:))) -// item.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") -// -// if selected { -// item.accessibilityTraits.insert(.selected) -// item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") -// } else { -// item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") -// } -// -// item.tintColor = UIColor.glucoseTintColor -// item.isEnabled = isEnabled -// -// return item -// } + private func createWorkoutButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { + let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .plain, target: self, action: #selector(toggleWorkoutMode(_:))) + item.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") + + if selected { + item.accessibilityTraits.insert(.selected) + item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") + } else { + item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") + } + + item.tintColor = UIColor.glucoseTintColor + item.isEnabled = isEnabled + + return item + } @objc func onPresetsTapped() { if preMealMode == true { @@ -1495,6 +1524,15 @@ final class StatusTableViewController: LoopChartsTableViewController { } func presentPresetAlertController() { + guard workoutModeAllowed else { + presentPreMealModeAlertController() + return + } + guard preMealModeAllowed else { + presentWorkoutModeAlertController() + return + } + let vc = UIAlertController(presetTypeSelectionHandler: { [self] presetType in switch presetType { case .preMeal: @@ -1509,15 +1547,15 @@ final class StatusTableViewController: LoopChartsTableViewController { present(vc, animated: true, completion: nil) } -// @IBAction func togglePreMealMode(_ sender: UIBarButtonItem) { -// if preMealMode == true { -// deviceManager.loopManager.mutateSettings { settings in -// settings.clearOverride(matching: .preMeal) -// } -// } else { -// presentPreMealModeAlertController() -// } -// } + @IBAction func togglePreMealMode(_ sender: UIBarButtonItem) { + if preMealMode == true { + deviceManager.loopManager.mutateSettings { settings in + settings.clearOverride(matching: .preMeal) + } + } else { + presentPreMealModeAlertController() + } + } func presentPreMealModeAlertController() { let vc = UIAlertController(premealDurationSelectionHandler: { duration in @@ -1544,19 +1582,19 @@ final class StatusTableViewController: LoopChartsTableViewController { present(vc, animated: true, completion: nil) } -// @IBAction func toggleWorkoutMode(_ sender: UIBarButtonItem) { -// if workoutMode == true { -// deviceManager.loopManager.mutateSettings { settings in -// settings.clearOverride() -// } -// } else { -// if FeatureFlags.sensitivityOverridesEnabled { -// performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) -// } else { -// presentWorkoutModeAlertController() -// } -// } -// } + @IBAction func toggleWorkoutMode(_ sender: UIBarButtonItem) { + if workoutMode == true { + deviceManager.loopManager.mutateSettings { settings in + settings.clearOverride() + } + } else { + if FeatureFlags.sensitivityOverridesEnabled { + performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) + } else { + presentWorkoutModeAlertController() + } + } + } func presentWorkoutModeAlertController() { let vc = UIAlertController(workoutDurationSelectionHandler: { duration in @@ -1675,7 +1713,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func automaticDosingStatusChanged(_ automaticDosingEnabled: Bool) { -// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) + updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) hudView?.loopCompletionHUD.loopIconClosed = automaticDosingEnabled hudView?.loopCompletionHUD.closedLoopDisallowedLocalizedDescription = deviceManager.closedLoopDisallowedLocalizedDescription } @@ -2282,3 +2320,12 @@ extension StatusTableViewController: ServicesViewModelDelegate { show(settingsViewController, sender: self) } } + +extension StatusTableViewController: FavoriteFoodsFeatureUnlockDelegate { + func featureAvailabilityChanged() { + deviceManager.loopManager.mutateSettings { settings in + settings.favoriteFoodsEnabled.toggle() + } + self.updateToolbarItems() + } +} diff --git a/Loop/Views/MealsView.swift b/Loop/Views/MealsView.swift index e565574850..5407aa3a5b 100644 --- a/Loop/Views/MealsView.swift +++ b/Loop/Views/MealsView.swift @@ -36,7 +36,7 @@ struct MealsView: View { VStack(spacing: 16) { VStack(spacing: 10) { HStack { - Text("All Meals") + Text("All Favorites") .font(.title2) .fontWeight(.semibold) .foregroundColor(.primary) @@ -61,18 +61,18 @@ struct MealsView: View { dismissButton } } - .navigationBarTitle(Text(NSLocalizedString("Meals", comment: "Meals screen title"))) + .navigationBarTitle(Text(NSLocalizedString("Favorite Foods", comment: "Favorite Foods screen title"))) .navigationViewStyle(.stack) - NavigationLink(destination: Text("Coming later: bolus screen for \(selectedMeal?.name ?? "")"), isActive: $isBolusViewActive) { + NavigationLink(destination: Text("Coming later:\nprepopulated carb entry screen").multilineTextAlignment(.center), isActive: $isBolusViewActive) { EmptyView() } - NavigationLink(destination: Text("Coming later: edit meal screen for \(selectedMeal?.name ?? "")"), isActive: $isEditViewActive) { + NavigationLink(destination: Text("Coming later:\nedit favorite food screen").multilineTextAlignment(.center), isActive: $isEditViewActive) { EmptyView() } - NavigationLink(destination: Text("Coming later: add meal screen"), isActive: $isAddViewActive) { + NavigationLink(destination: Text("Coming later:\nadd favorite food screen").multilineTextAlignment(.center), isActive: $isAddViewActive) { EmptyView() } } @@ -164,7 +164,7 @@ extension MealsView { HStack { Image(systemName: "plus.circle.fill") - Text("Add a new meal") + Text("Add a new favorite food") } } .buttonStyle(ActionButtonStyle()) diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift index 63418ad4aa..7051c4d695 100644 --- a/LoopCore/LoopSettings.swift +++ b/LoopCore/LoopSettings.swift @@ -74,6 +74,8 @@ public struct LoopSettings: Equatable { public var automaticDosingStrategy: AutomaticDosingStrategy = .tempBasalOnly public var defaultRapidActingModel: ExponentialInsulinModelPreset? + + public var favoriteFoodsEnabled = false public var glucoseUnit: HKUnit? { return glucoseTargetRangeSchedule?.unit @@ -94,7 +96,8 @@ public struct LoopSettings: Equatable { maximumBolus: Double? = nil, suspendThreshold: GlucoseThreshold? = nil, automaticDosingStrategy: AutomaticDosingStrategy = .tempBasalOnly, - defaultRapidActingModel: ExponentialInsulinModelPreset? = nil + defaultRapidActingModel: ExponentialInsulinModelPreset? = nil, + favoriteFoodsEnabled: Bool = false ) { self.dosingEnabled = dosingEnabled self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule @@ -111,6 +114,7 @@ public struct LoopSettings: Equatable { self.suspendThreshold = suspendThreshold self.automaticDosingStrategy = automaticDosingStrategy self.defaultRapidActingModel = defaultRapidActingModel + self.favoriteFoodsEnabled = favoriteFoodsEnabled } } @@ -277,6 +281,10 @@ extension LoopSettings: RawRepresentable { { self.automaticDosingStrategy = automaticDosingStrategy } + + if let favoriteFoodsEnabled = rawValue["favoriteFoodsEnabled"] as? Bool { + self.favoriteFoodsEnabled = favoriteFoodsEnabled + } } public var rawValue: RawValue { @@ -295,7 +303,8 @@ extension LoopSettings: RawRepresentable { raw["maximumBolus"] = maximumBolus raw["minimumBGGuard"] = suspendThreshold?.rawValue raw["dosingStrategy"] = automaticDosingStrategy.rawValue - + raw["favoriteFoodsEnabled"] = favoriteFoodsEnabled + return raw } } From 1256b0ee66519a5f3e66d08a491d3da4a44ffef6 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Mon, 24 Jul 2023 16:14:18 -0400 Subject: [PATCH 04/22] [LOOP-4685] Add favorite foods to user defaults; renaming --- Loop.xcodeproj/project.pbxproj | 8 +-- Loop/Extensions/UserDefaults+Loop.swift | 10 +++ Loop/Managers/SettingsManager.swift | 4 +- .../StatusTableViewController.swift | 26 ++++--- ...ealsView.swift => FavoriteFoodsView.swift} | 72 +++++++++---------- LoopCore/LoopSettings.swift | 11 +-- 6 files changed, 64 insertions(+), 67 deletions(-) rename Loop/Views/{MealsView.swift => FavoriteFoodsView.swift} (72%) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 5eb0f7daf3..3031343dee 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 1419606928D9554E00BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; }; 1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; }; 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB7582A60BF2E0075748A /* EditMode.swift */; }; - 142CB75B2A60BFC30075748A /* MealsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB75A2A60BFC30075748A /* MealsView.swift */; }; + 142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; @@ -788,7 +788,7 @@ /* Begin PBXFileReference section */ 142CB7582A60BF2E0075748A /* EditMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMode.swift; sourceTree = ""; }; - 142CB75A2A60BFC30075748A /* MealsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealsView.swift; sourceTree = ""; }; + 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = ""; }; 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SmallStatusWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -2389,12 +2389,12 @@ 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */, A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */, C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */, + 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */, 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */, B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */, 430D85881F44037000AF2D4F /* HUDViewTableViewCell.swift */, A91D2A3E26CF0FF80023B075 /* IconTitleSubtitleTableViewCell.swift */, C1742331259BEADC00399C9D /* ManualEntryDoseView.swift */, - 142CB75A2A60BFC30075748A /* MealsView.swift */, 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */, 899433B723FE129700FA4BEA /* OverrideBadgeView.swift */, 89D6953D23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift */, @@ -3856,7 +3856,7 @@ 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */, 1DA649A9244126DA00F61E75 /* InAppModalAlertScheduler.swift in Sources */, 43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */, - 142CB75B2A60BFC30075748A /* MealsView.swift in Sources */, + 142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */, A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */, diff --git a/Loop/Extensions/UserDefaults+Loop.swift b/Loop/Extensions/UserDefaults+Loop.swift index dae95c9611..f3bfe41293 100644 --- a/Loop/Extensions/UserDefaults+Loop.swift +++ b/Loop/Extensions/UserDefaults+Loop.swift @@ -16,6 +16,7 @@ extension UserDefaults { case legacyServicesState = "com.loopkit.Loop.ServicesState" case loopNotRunningNotifications = "com.loopkit.Loop.loopNotRunningNotifications" case inFlightAutomaticDose = "com.loopkit.Loop.inFlightAutomaticDose" + case favoriteFoodsEnabled = "com.loopkit.Loop.favoriteFoodsEnabled" } var legacyPumpManagerRawValue: PumpManager.RawValue? { @@ -89,4 +90,13 @@ extension UserDefaults { } } } + + var favoriteFoodsEnabled: Bool { + get { + bool(forKey: Key.favoriteFoodsEnabled.rawValue) + } + set { + set(newValue, forKey: Key.favoriteFoodsEnabled.rawValue) + } + } } diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index e7ee416396..f7421b97f7 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -109,8 +109,7 @@ class SettingsManager { maximumBolus: latestSettings.maximumBolus, suspendThreshold: latestSettings.suspendThreshold, automaticDosingStrategy: latestSettings.automaticDosingStrategy, - defaultRapidActingModel: latestSettings.defaultRapidActingModel?.presetForRapidActingInsulin, - favoriteFoodsEnabled: latestSettings.favoriteFoodsEnabled) + defaultRapidActingModel: latestSettings.defaultRapidActingModel?.presetForRapidActingInsulin) } } @@ -133,7 +132,6 @@ class SettingsManager { deviceToken: deviceToken, insulinType: deviceStatusProvider?.pumpManagerStatus?.insulinType, defaultRapidActingModel: newLoopSettings.defaultRapidActingModel.map(StoredInsulinModel.init), - favoriteFoodsEnabled: newLoopSettings.favoriteFoodsEnabled, basalRateSchedule: newLoopSettings.basalRateSchedule, insulinSensitivitySchedule: newLoopSettings.insulinSensitivitySchedule, carbRatioSchedule: newLoopSettings.carbRatioSchedule, diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 7a1eb35707..54deebf554 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -274,14 +274,14 @@ final class StatusTableViewController: LoopChartsTableViewController { let bolus = UIBarButtonItem(image: UIImage(named: "bolus"), style: .plain, target: self, action: #selector(presentBolusScreen)) let settings = UIBarButtonItem(image: UIImage(named: "settings"), style: .plain, target: self, action: #selector(onSettingsTapped)) - let favoriteFoodsEnabled = deviceManager.loopManager.settings.favoriteFoodsEnabled - if favoriteFoodsEnabled { - let meals = UIBarButtonItem(image: UIImage(named: "meals"), style: .plain, target: self, action: #selector(presentMealsScreen)) + let favoriteFoodsEnabled = UserDefaults.appGroup?.favoriteFoodsEnabled + if favoriteFoodsEnabled == true { + let favoriteFoods = UIBarButtonItem(image: UIImage(named: "favorite-foods"), style: .plain, target: self, action: #selector(presentFavoriteFoodsScreen)) let presets = createPresetsButtonItem(selected: false, isEnabled: true) toolbarItems = [ carbs, space, - meals, + favoriteFoods, space, bolus, space, @@ -319,8 +319,8 @@ final class StatusTableViewController: LoopChartsTableViewController { toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") toolbarItems![8].tintColor = UIColor.secondaryLabel - let favoriteFoodsEnabled = deviceManager.loopManager.settings.favoriteFoodsEnabled - if favoriteFoodsEnabled { + let favoriteFoodsEnabled = UserDefaults.appGroup?.favoriteFoodsEnabled + if favoriteFoodsEnabled == true { toolbarItems![2] = createFavoriteFoodsButtonItem() let selected = (preMealMode == true && preMealModeAllowed) || (workoutMode == true && workoutModeAllowed) @@ -551,7 +551,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } } -// updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) + updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) if deviceManager.loopManager.settings.preMealTargetRange == nil { preMealMode = nil @@ -1414,11 +1414,11 @@ final class StatusTableViewController: LoopChartsTableViewController { presentBolusEntryView() } - @objc func presentMealsScreen() { + @objc func presentFavoriteFoodsScreen() { let hostingController: DismissibleHostingController - let mealsView = MealsView().environmentObject(deviceManager.displayGlucosePreference) - hostingController = DismissibleHostingController(rootView: mealsView, isModalInPresentation: false) + let favoriteFoodsView = FavoriteFoodsView().environmentObject(deviceManager.displayGlucosePreference) + hostingController = DismissibleHostingController(rootView: favoriteFoodsView, isModalInPresentation: false) present(hostingController, animated: true) } @@ -1444,7 +1444,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func createFavoriteFoodsButtonItem() -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage(named: "favorite-foods")!, style: .plain, target: self, action: #selector(presentMealsScreen)) + let item = UIBarButtonItem(image: UIImage(named: "favorite-foods")!, style: .plain, target: self, action: #selector(presentFavoriteFoodsScreen)) item.accessibilityLabel = NSLocalizedString("Favorite Foods", comment: "The label of the favorite foods button") item.tintColor = UIColor.carbTintColor @@ -2323,9 +2323,7 @@ extension StatusTableViewController: ServicesViewModelDelegate { extension StatusTableViewController: FavoriteFoodsFeatureUnlockDelegate { func featureAvailabilityChanged() { - deviceManager.loopManager.mutateSettings { settings in - settings.favoriteFoodsEnabled.toggle() - } + UserDefaults.appGroup?.favoriteFoodsEnabled.toggle() self.updateToolbarItems() } } diff --git a/Loop/Views/MealsView.swift b/Loop/Views/FavoriteFoodsView.swift similarity index 72% rename from Loop/Views/MealsView.swift rename to Loop/Views/FavoriteFoodsView.swift index 5407aa3a5b..6abb06848a 100644 --- a/Loop/Views/MealsView.swift +++ b/Loop/Views/FavoriteFoodsView.swift @@ -1,5 +1,5 @@ // -// MealsView.swift +// FavoriteFoodsView.swift // Loop // // Created by Noah Brauner on 7/12/23. @@ -11,23 +11,23 @@ import LoopKit import LoopKitUI import HealthKit -struct MealsView: View { +struct FavoriteFoodsView: View { @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference @Environment(\.dismissAction) private var dismiss @Environment(\.carbTintColor) private var carbTintColor - @State private var mealToConfirmDeleteId: String? = nil + @State private var foodToConfirmDeleteId: String? = nil @State private var editMode: EditMode = .inactive - @State private var meals = allMeals + @State private var foods = allFoods @State var isBolusViewActive = false @State var isEditViewActive = false @State var isAddViewActive = false - @State var selectedMeal: Meal? = nil + @State var selectedFood: FavoriteFood? = nil - @State private var draggingMeal: Meal? + @State private var draggingFood: FavoriteFood? @State private var hasChangedLocation: Bool = false var body: some View { @@ -46,13 +46,13 @@ struct MealsView: View { editButton } - ForEach(meals) { meal in - draggableMealCardView(meal: meal) + ForEach(foods) { food in + draggableFoodCardView(food: food) } } .environment(\.editMode, self.$editMode) - newMealButton + newFoodButton } .padding() } @@ -78,17 +78,17 @@ struct MealsView: View { } .onChange(of: editMode) { newValue in if !newValue.isEditing { - mealToConfirmDeleteId = nil + foodToConfirmDeleteId = nil } } } - private func addMeal() { + private func addFood() { isAddViewActive = true } - private func onMealTap(_ meal: Meal) { - selectedMeal = meal + private func onFoodTap(_ food: FavoriteFood) { + selectedFood = food if editMode.isEditing { isEditViewActive = true } @@ -96,40 +96,40 @@ struct MealsView: View { isBolusViewActive = true } } - private func onMealDelete(_ meal: Meal) { + private func onFoodDelete(_ food: FavoriteFood) { withAnimation(.easeInOut(duration: 0.3)) { - _ = meals.remove(meal) + _ = foods.remove(food) } } - private func onMealReorder(from: IndexSet, to: Int) { + private func onFoodReorder(from: IndexSet, to: Int) { withAnimation { - meals.move(fromOffsets: from, toOffset: to) + foods.move(fromOffsets: from, toOffset: to) } } } -extension MealsView { - @ViewBuilder func draggableMealCardView(meal: Meal) -> some View { +extension FavoriteFoodsView { + @ViewBuilder func draggableFoodCardView(food: FavoriteFood) -> some View { Button(action: { - onMealTap(meal) + onFoodTap(food) }) { - MealCardView(meal: meal, mealToConfirmDeleteId: $mealToConfirmDeleteId, onMealTap: onMealTap(_:), onMealDelete: onMealDelete(_:)) + FavoriteFoodCardView(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: onFoodDelete(_:)) .onDrag { - draggingMeal = meal - return NSItemProvider(object: "\(meal.id)" as NSString) + draggingFood = food + return NSItemProvider(object: "\(food.id)" as NSString) } preview: { - MealCardView(meal: meal, mealToConfirmDeleteId: $mealToConfirmDeleteId, onMealTap: onMealTap(_:), onMealDelete: onMealDelete(_:)) + FavoriteFoodCardView(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: onFoodDelete(_:)) } .onDrop( of: [UTType.text], delegate: DragRelocateDelegate( - item: meal, - listData: meals, - current: $draggingMeal, + item: food, + listData: foods, + current: $draggingFood, hasChangedLocation: $hasChangedLocation ) { from, to in - onMealReorder(from: from, to: to) + onFoodReorder(from: from, to: to) } ) .disabled(!editMode.isEditing) @@ -144,7 +144,7 @@ extension MealsView { } // private var plusButton: some View { -// Button(action: addMeal) { +// Button(action: addFood) { // Image(systemName: "plus") // } // } @@ -159,8 +159,8 @@ extension MealsView { } } - private var newMealButton: some View { - Button(action: addMeal) { + private var newFoodButton: some View { + Button(action: addFood) { HStack { Image(systemName: "plus.circle.fill") @@ -221,12 +221,12 @@ fileprivate struct DragRelocateDelegate: DropDelegate { } } -fileprivate let allMeals = [ +fileprivate let allFoods = [ // Some really yummy foods... - Meal(carbsQuantity: carbs(55), foodType: "🥞🥚", absorptionTime: .hours(3), name: "Pancakes and Eggs"), - Meal(carbsQuantity: carbs(35), foodType: "🍌🍞", absorptionTime: .hours(2), name: "Banana Bread"), - Meal(carbsQuantity: carbs(63), foodType: "🍞🥜🍫🥛", absorptionTime: .hours(3), name: "The Best Lunch"), - Meal(carbsQuantity: carbs(120), foodType: "🍕", absorptionTime: .hours(5), name: "Dad's Pizza"), + FavoriteFood(carbsQuantity: carbs(55), foodType: "🥞🥚", absorptionTime: .hours(3), name: "Pancakes and Eggs"), + FavoriteFood(carbsQuantity: carbs(35), foodType: "🍌🍞", absorptionTime: .hours(2), name: "Banana Bread"), + FavoriteFood(carbsQuantity: carbs(63), foodType: "🍞🥜🍫🥛", absorptionTime: .hours(3), name: "The Best Lunch"), + FavoriteFood(carbsQuantity: carbs(120), foodType: "🍕", absorptionTime: .hours(5), name: "Dad's Pizza"), ] fileprivate func carbs(_ value: Double) -> HKQuantity { diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift index 7051c4d695..82ad76b6cc 100644 --- a/LoopCore/LoopSettings.swift +++ b/LoopCore/LoopSettings.swift @@ -75,8 +75,6 @@ public struct LoopSettings: Equatable { public var defaultRapidActingModel: ExponentialInsulinModelPreset? - public var favoriteFoodsEnabled = false - public var glucoseUnit: HKUnit? { return glucoseTargetRangeSchedule?.unit } @@ -96,8 +94,7 @@ public struct LoopSettings: Equatable { maximumBolus: Double? = nil, suspendThreshold: GlucoseThreshold? = nil, automaticDosingStrategy: AutomaticDosingStrategy = .tempBasalOnly, - defaultRapidActingModel: ExponentialInsulinModelPreset? = nil, - favoriteFoodsEnabled: Bool = false + defaultRapidActingModel: ExponentialInsulinModelPreset? = nil ) { self.dosingEnabled = dosingEnabled self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule @@ -114,7 +111,6 @@ public struct LoopSettings: Equatable { self.suspendThreshold = suspendThreshold self.automaticDosingStrategy = automaticDosingStrategy self.defaultRapidActingModel = defaultRapidActingModel - self.favoriteFoodsEnabled = favoriteFoodsEnabled } } @@ -281,10 +277,6 @@ extension LoopSettings: RawRepresentable { { self.automaticDosingStrategy = automaticDosingStrategy } - - if let favoriteFoodsEnabled = rawValue["favoriteFoodsEnabled"] as? Bool { - self.favoriteFoodsEnabled = favoriteFoodsEnabled - } } public var rawValue: RawValue { @@ -303,7 +295,6 @@ extension LoopSettings: RawRepresentable { raw["maximumBolus"] = maximumBolus raw["minimumBGGuard"] = suspendThreshold?.rawValue raw["dosingStrategy"] = automaticDosingStrategy.rawValue - raw["favoriteFoodsEnabled"] = favoriteFoodsEnabled return raw } From ea9e39bfe0907332eeb5bc6fc6fcbffacf25e166 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Tue, 25 Jul 2023 15:54:01 -0400 Subject: [PATCH 05/22] Fix accessibility label for UI test --- Loop/View Controllers/StatusTableViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 54deebf554..d3f0a2ebb7 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -310,7 +310,7 @@ final class StatusTableViewController: LoopChartsTableViewController { private func updateToolbarItems() { let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true - toolbarItems![0].accessibilityLabel = NSLocalizedString("Enter Carbs", comment: "The label of the carb entry button") + toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") toolbarItems![0].isEnabled = isPumpOnboarded toolbarItems![0].tintColor = UIColor.carbTintColor toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") From e5efa6b343f2bc2eb534a11323273af045a7c66a Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 27 Jul 2023 16:43:17 -0500 Subject: [PATCH 06/22] Remove CURRENT_PROJECT_VERSION override --- Loop.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index aeca446056..6583462e89 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -411,6 +411,7 @@ C159C82A286785E300A86EC0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C159C8212867859800A86EC0 /* MockKitUI.framework */; }; C159C82D2867876500A86EC0 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F70C1DD1DE8DCA7006380B7 /* NotificationCenter.framework */; }; C159C82F286787EF00A86EC0 /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C159C82E286787EF00A86EC0 /* LoopKit.framework */; }; + C15A8C492A7305B1009D736B /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1E3DC4628595FAA00CA19FF /* SwiftCharts */; }; C165756F2534C468004AE16E /* SimpleBolusViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C165756E2534C468004AE16E /* SimpleBolusViewModelTests.swift */; }; C16575712538A36B004AE16E /* CGMStalenessMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16575702538A36B004AE16E /* CGMStalenessMonitor.swift */; }; C16575732538AFF6004AE16E /* CGMStalenessMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16575722538AFF6004AE16E /* CGMStalenessMonitorTests.swift */; }; @@ -1755,6 +1756,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C15A8C492A7305B1009D736B /* SwiftCharts in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4752,7 +4754,6 @@ CODE_SIGN_ENTITLEMENTS = "Loop Widget Extension/Bootstrap/LoopWidgetExtension.entitlements"; CODE_SIGN_IDENTITY = "$(LOOP_CODE_SIGN_IDENTITY_DEBUG)"; CODE_SIGN_STYLE = "$(LOOP_CODE_SIGN_STYLE)"; - CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -4797,7 +4798,6 @@ CODE_SIGN_ENTITLEMENTS = "Loop Widget Extension/Bootstrap/LoopWidgetExtension.entitlements"; CODE_SIGN_IDENTITY = "$(LOOP_CODE_SIGN_IDENTITY_RELEASE)"; CODE_SIGN_STYLE = "$(LOOP_CODE_SIGN_STYLE)"; - CURRENT_PROJECT_VERSION = 101; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; From 3f5b148605db108db90ba730c492072a56ccb3cf Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 31 Jul 2023 16:58:23 -0500 Subject: [PATCH 07/22] Recompute insulin effect if insulinEffectStartDate is earlier than last computation --- Loop/Info.plist | 4 ++-- Loop/Managers/LoopDataManager.swift | 2 +- Loop/View Controllers/RootNavigationController.swift | 2 +- .../View Controllers/StatusTableViewController.swift | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Loop/Info.plist b/Loop/Info.plist index d602518d3d..ddad5426ac 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -2,8 +2,6 @@ - NFCReaderUsageDescription - The app uses NFC to pair with diabetes devices. AppGroupIdentifier $(APP_GROUP_IDENTIFIER) AppStoreURL @@ -57,6 +55,8 @@ LoopLocalCacheDurationDays $(LOOP_LOCAL_CACHE_DURATION_DAYS) + NFCReaderUsageDescription + The app uses NFC to pair with diabetes devices. NSBluetoothAlwaysUsageDescription The app needs to use Bluetooth to send and receive data from your diabetes devices. NSBluetoothPeripheralUsageDescription diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index f6f4b04c38..c4fe0fa3c0 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -999,7 +999,7 @@ extension LoopDataManager { } } - if insulinEffect == nil { + if insulinEffect == nil || insulinEffect?.first?.startDate ?? .distantFuture > insulinEffectStartDate { self.logger.debug("Recomputing insulin effects") updateGroup.enter() doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: now()) { (result) -> Void in diff --git a/Loop/View Controllers/RootNavigationController.swift b/Loop/View Controllers/RootNavigationController.swift index 7a4b2facf9..910ada7c79 100644 --- a/Loop/View Controllers/RootNavigationController.swift +++ b/Loop/View Controllers/RootNavigationController.swift @@ -22,7 +22,7 @@ class RootNavigationController: UINavigationController { case .carbEntry: statusTableViewController.presentCarbEntryScreen(nil) case .preMeal: - statusTableViewController.presentPreMealMode() + statusTableViewController.togglePreMealMode() case .bolus: statusTableViewController.presentBolusScreen() case .customPresets: diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index e0b9f82df3..5f2c0f4fbb 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1469,7 +1469,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(togglePreMealMode(_:))) + let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(premealButtonTapped(_:))) item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") if selected { @@ -1545,8 +1545,12 @@ final class StatusTableViewController: LoopChartsTableViewController { present(vc, animated: true, completion: nil) } + + @IBAction func premealButtonTapped(_ sender: UIBarButtonItem) { + togglePreMealMode(confirm: false) + } - @IBAction func togglePreMealMode(_ sender: UIBarButtonItem) { + func togglePreMealMode(confirm: Bool = true) { if preMealMode == true { if confirm { let alert = UIAlertController(title: "Disable Pre-Meal Preset?", message: "This will remove any currently applied pre-meal preset.", preferredStyle: .alert) @@ -1592,10 +1596,6 @@ final class StatusTableViewController: LoopChartsTableViewController { present(vc, animated: true, completion: nil) } - @IBAction func togglePreMealMode(_ sender: UIBarButtonItem) { - presentPreMealMode(confirm: false) - } - func presentCustomPresets(confirm: Bool = true) { if workoutMode == true { if confirm { From 480ad6fe4066e8220920aefb3ac23370455d2e81 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 9 Aug 2023 10:54:42 -0500 Subject: [PATCH 08/22] Update error messages --- Loop/Managers/LoopDataManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index c4fe0fa3c0..6366084a46 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1005,7 +1005,7 @@ extension LoopDataManager { doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: now()) { (result) -> Void in switch result { case .failure(let error): - self.logger.error("%{public}@", String(describing: error)) + self.logger.error("Could not fetch insulin effects: %{public}@", error.localizedDescription) self.insulinEffect = nil warnings.append(.fetchDataWarning(.insulinEffect(error: error))) case .success(let effects): @@ -1021,7 +1021,7 @@ extension LoopDataManager { doseStore.getGlucoseEffects(start: insulinEffectStartDate, end: nil, basalDosingEnd: nil) { (result) -> Void in switch result { case .failure(let error): - self.logger.error("Could not fetch insulin effects: %{public}@", String(describing: error)) + self.logger.error("Could not fetch insulin effects including pending insulin: %{public}@", error.localizedDescription) self.insulinEffectIncludingPendingInsulin = nil warnings.append(.fetchDataWarning(.insulinEffectIncludingPendingInsulin(error: error))) case .success(let effects): From fd8f1f9c0877a5aaf953629e0458bc870d00535a Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Wed, 9 Aug 2023 19:19:53 -0400 Subject: [PATCH 09/22] [LOOP-4691] Move favorite foods to settings --- .../Contents.json | 23 +++ .../Favorite Foods Icon.png | Bin 0 -> 567 bytes .../Favorite Foods Icon@2x.png | Bin 0 -> 1176 bytes .../Favorite Foods Icon@3x.png | Bin 0 -> 1734 bytes .../favorite-foods.imageset/Contents.json | 16 -- .../favorite-foods.imageset/meals.pdf | Bin 2234 -> 0 bytes .../presets-selected.imageset/Contents.json | 16 -- .../presets-selected.pdf | Bin 2876 -> 0 bytes .../presets.colorset/Contents.json | 38 ----- .../presets.imageset/Contents.json | 16 -- .../presets.imageset/presets.pdf | Bin 3000 -> 0 bytes .../StatusTableViewController.swift | 146 +++--------------- Loop/Views/SettingsView.swift | 15 ++ 13 files changed, 59 insertions(+), 211 deletions(-) create mode 100644 Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Contents.json create mode 100644 Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Favorite Foods Icon.png create mode 100644 Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Favorite Foods Icon@2x.png create mode 100644 Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Favorite Foods Icon@3x.png delete mode 100644 Loop/DefaultAssets.xcassets/favorite-foods.imageset/Contents.json delete mode 100644 Loop/DefaultAssets.xcassets/favorite-foods.imageset/meals.pdf delete mode 100644 Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json delete mode 100644 Loop/DefaultAssets.xcassets/presets-selected.imageset/presets-selected.pdf delete mode 100644 Loop/DefaultAssets.xcassets/presets.colorset/Contents.json delete mode 100644 Loop/DefaultAssets.xcassets/presets.imageset/Contents.json delete mode 100644 Loop/DefaultAssets.xcassets/presets.imageset/presets.pdf diff --git a/Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Contents.json b/Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Contents.json new file mode 100644 index 0000000000..e910256d55 --- /dev/null +++ b/Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Favorite Foods Icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Favorite Foods Icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Favorite Foods Icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Favorite Foods Icon.png b/Loop/DefaultAssets.xcassets/Favorite Foods Icon.imageset/Favorite Foods Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..794ae3f5a7733b19b0044f9d3cf8e44c7f0b3e10 GIT binary patch literal 567 zcmV-70?7S|P)`a+`C~?L_kk-& zfV3LpgYnsTG@hKV#=WsL7RW0VXJp2?3)nf3iQrQrLF2AXM0X%a;J%ks+k19FoE@Ws zaU4+_F7)2HBt{al6t1Q%OB2F%Zv^M9Je+uPqN_{x>2lelCf+xXq9i7=(?sizRJZ}@ zqNl{M)6+Z`QIfSma7}1GBC4ZEEg{AQpuZfkbf&#PUX)IfAD3W2_Q*t+{-4YtsGZen8xE^aYS9xM4%g(5GMk=H+KgkP+#}stW<*c$-FXY2>&oPImOX z1o$-)!woJ*3FOk#xJeem`D-PR5*Zx};YuJCi0TXBwm_c2P%1G8jsrPh{nKnw0*~wE zkqNg1*{*b&ErG|)_5s@_;Vb?&@KxYakhNR|?pcsk;c7#!ZKSF|mV5+kdRw`&33~A1 zH2wiDlm-v(B~r-4DZE$;Zw%)p@%S&S#2e}p_v>!4egS&+5kF%wE=8kbppD9=>)KWvq9J(wSr88>mONq`U&Ja=bb>} zNcUaKwj^7*Kp2}YuDbZ%1=+thHhNyVc(t#8x_Gkx$H3Gpy11UuW9r@7*IvsLG9bEW zy4ZEG_XzV{2_81Ktj1G|iQ48A9T6yW7;P67KDJm_Huk%|J8?>nL<9)D#)S$u+j1H= zj7OU@3%8NQ4i6=o(PCO%lSqrn`3`|pNF#RG8fiq!mHjNWo(i7SKytCid;ba_%}aF@ zj75!=R6sVdLlbkG*GC0MZh`D#k4-GWygnAsiBy>2(qe~&tMhXqLQ9SY6Uc%@mL8%v z?c&J57L1+8Oqen5FT9PrxAr}A5xVZ;27Ekq@$L8xfzxUMV~3T%JOtiOxHvAnW-Pc* z$n0^6xSe00(Sl~o@g2^j+JRs~9219~ZC5tn>1ph2S?q`JSc@R=nz-1VSU@r95SQSe z2YVTLZ;Q8+?iM`!3@qz6csN{`%+w8M3!IzVBZRla8a#01de{-%o#!P8VI~rI;K;Q# zhp^9V@Eo-8`4;|%vj)#;fHmhmbGUX`3F}*;01pBP&hrqL;OB?qc3Z4(i7((mfRTNK zAh=_;Z5_(ofseH;6qD+1T!nE6?&kH|?yOO@jYp)*gNCAXqKd~% zBoFiU8KWK(CK8LNPSm_TW=2{~Vj=ffcflYO+WLA#W^o$J5b~NxO9Huj>zwBG5b~H< zJ7~-De2kvaVs_|e;+eC?#GzmoMiMnaTn4Bio@PH8a zP}Vc`wiDmUVPcDb8(BAzMCpn82+KyKHc=pu2WCNfq8@^~0jW$>V4`6Xq$kSvc@8?Z ziH~4p!yrmelpXRAc`OrK@ZT>F5~U}~#+j?Iz=CQ%JO^B(X;;kae0@WoL$n-`42dh;aCd_cm@xRV~5#fC6{acNLk qAMXNXLiqR7cZMzA)}_6oF#ZBWzzpJMJaiQR0000mlHm^BL0Qna zyk`a<_c;7g_rSJ&Rjh|?6Th&}bAQJV2R`^W5#>ln^70c^E?TkA<|q;|VY=v_>2=jZU9`zkX%eyVk%6wde*`P`BeO)jn4@^x{o}{pn>mTG zn6B`yiAP>$oOH8P%nT2WAB@94hTc%bHK_0rCb@9KIC-jx0zXNj!83)@LqTGSf_bM{ z3i*6@F;tLRxDZ{K?V8JbSrZ#iWjGWzl09A0NUF?F{Q_vgjrSX`e^i8&`@X>*rk*v3 zl-q=Uz420T!>>azObC(-?4tpx^1sWheDX0r&yXLvejMJ$dV!3&Y~N$-LN(!|AQ>I$hNcS#`>XI z&Qi%^5-zql2X>O-^E=p(vz(>h$&#lnS9qXCc(20E5j6Km&XA)lvR zhh{UnlB)>gu@G-gKXm{FvMs@f+)sovBUJSZR|Qfq8!{y2nr)e*r6!ym8(ZA6O-hc1y{pD&Th?-o43p7BbJRt za>ha`U>9)AJr!DmK6u{0yNL@GxS#p686|fTw>8nB^O1?o5bBV5I zopM8zT;t)lCKT=~=lgnY2$E|&{=9-_y>dg4?DDRQ(YQ#0gf3cURjeAZ?wt7zCoOr-jF=4(aA+Fq zp(u~6H=nUipOTA->#K-}If;-%70f5DD+I~qg!K&lc17?grc2fX>&YjsBLvB6!snxu z+o6~)`7?XbX24ZW{H(7i+cGQmd4aK`=&0p(DAx0f+cskrayt}B@@nOFD3avW%I#2` zw&YdI?NFSiv#LDoq~AeW+Y~j5w=)Yk0m}SG-2LhUz6lo!!)F6(2(RR z5>`=!Avwu)h{CVrle}mkjlxgM5`0S^xk507*qoM6N<$f^bzui2wiq literal 0 HcmV?d00001 diff --git a/Loop/DefaultAssets.xcassets/favorite-foods.imageset/Contents.json b/Loop/DefaultAssets.xcassets/favorite-foods.imageset/Contents.json deleted file mode 100644 index 514195af8b..0000000000 --- a/Loop/DefaultAssets.xcassets/favorite-foods.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "meals.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/Loop/DefaultAssets.xcassets/favorite-foods.imageset/meals.pdf b/Loop/DefaultAssets.xcassets/favorite-foods.imageset/meals.pdf deleted file mode 100644 index a477df6b919c46daca6e107d627791c473c1b299..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2234 zcmZuz(QaEe41L#E=p{fqpt49xlmrw7)?^)qZCK{+t=NO-+@>BJ-_3T1u3x`H*}is? zIY6o$iz3fCl-R4=n|H4$6z9lCKmPHj^ZxZ~|K^PwHh1=?<4b(@)3CYUfARuwZo78- z?XY-ovzy_c)pD4B|JGl>zx->tbALMuv44C#EnZEl={r7s$AOC|iC;drY1{L~Zn<%+ zAy<+}o+PAFGH3tbI0aReT0J4GY0lMi3@x_C>@{eM8nY)2kxOCm$fzmS=$DRis6`q# zfW(Pa{n}9ql}J+b6e#7a2$n!4XW|P85yXv?g;L}S297ASA{6aB1}p04QK+>lM}R_H zOSL2iu6mwNnU8pF&lps;FAaCqhkd6ZOke)G$crbnDb66fm(zbIFVos96>@bBFJ)? z8ZoLRTb`*##MD+APPcQiMu9uUYODzo8s~;XQ8-tKlg#)oPJ#or6J@so-yx!cL$B;v z%Prj}hlUU&03++G)?4ktOhrr70(~W*HK-h11);tbl%&+iuo}uKH|UD0A~bK=OgC8p zwU9ug6<^HO&L;*e z!VOQ8B|XNOQ*f9n0aJUI6owuV5KXqkSI0Q$0oS$L1nF{AtRZcRC9zdzg2#mR?j}^x z3OjN@gD|1vn!Pw^`ECbZp4hEVxi-iNp-Lzf%zE22E-Chy+7dO~EWB5CLAl=?vIn)V zaJ#`4_XUp}+Nm8}$Yfm5et6YHaSrZXGV#r}XN_d{<>NPQ<9>BEW{xxDG;nOT90n1g zr>8lTCK&T+_U&df?sxvf@0eHk%zpd#uffl57x%*v_<6WnE#8h_{DkkQ8B3A|lwfqA^59l98r22I0s_T%afrFvs9_oqD9 z<7dRuv-)>HE>6-%CmF%rl<-Xada+-u$NR^jU$(1)3|nohC@eEi_i?km5U zAUu(eAySO8ClH3mQwaCzDP#}TDR94Ctk%QUnR`FnK#mu?@nLxBU##w49z>bV$8qnC ea2I&?e)V-g_5FVti|xMC!BfWW>gu~+-u(xIThJc> diff --git a/Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json b/Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json deleted file mode 100644 index 92eafb4695..0000000000 --- a/Loop/DefaultAssets.xcassets/presets-selected.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "presets-selected.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/Loop/DefaultAssets.xcassets/presets-selected.imageset/presets-selected.pdf b/Loop/DefaultAssets.xcassets/presets-selected.imageset/presets-selected.pdf deleted file mode 100644 index df10a133c2c2ac795504c2abe28e5ad952379130..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2876 zcmZuzO>Y}F5WVwP@M0h-5Q@WZ0zrVrZi=EU>MFejJ*exA<3g5NNh#9)`aZ6>Gb=l5 zz@XmF4rksx4(ICj=G`kYSt-(1@BjEyY5n@Oe)C3+ySw&Nxh1~(Y24kPK4}kd-K>tM z!?<}=tDEtkhhbcQ|5jhWU;Y}7>TgBHPLH|>0Lj4l~+rdnq`>=+vq1rxoEG|@I&MpMa6 zdy7W|W__gP)>D~NCZEDZK&}|?u+*%vf+6op z#wwe6Z^n>Mj}w_Qx>a<}C_})`q*N1F=$SiYL?%)6#p?w}jKODT6J=y{BHdMl#tzTL zi6|oPJg8pA%W!(RQ3_xNN=f82dE;ZK(P}EjW?K?g4ky93 zlR(uZL=h(1JBIQT#FLA|=4=U$Gg*uBiAU#+v$jCEviDMug5p%Od;}GlfsCH_1;zv| zP-b%W7_<;Bf)or9^*HOo5)t)`z-KYZlTDDOWDaIZ^aKusBgVkHu*vAm`owkj2yChk z{IFJ7l1s4ns`K4~B%7+zxmBI9+=@amKpN4-T8hr5MHn2EK^Z+O2{QL790j)GHbuG` zIQJRU^>YQGSmU!z=c4y6B^VO!T?!zg{WCdbQ;E(N+v95-U17C1$z#HjkqV=JoQ)Dyk>IIt4OK+lV$Gs_KFlmEz?Tm1` z=-D_l76(ka_YA>bC$Qbi-L78-?f&S_dRwZ?KzhxhV7N)ol}&=7Gt7$Ui7t??k=Btu zexr8kSJeicKE=!YQLk<{_hScs z9`7DDZ}(sH2bp{L5|*hzRc@vWgsNKxYd!CCz5k4F=~?_oAPZ0F!c$A|$Qw%H*PGL3yT8Bu^vhxVsI1QF zT#@!)r`SdZ!2Kbsr-Y!{u>efH0N#(rzstmVo+CC-~ICLf8=>dL;wH) diff --git a/Loop/DefaultAssets.xcassets/presets.colorset/Contents.json b/Loop/DefaultAssets.xcassets/presets.colorset/Contents.json deleted file mode 100644 index ec9cb985f4..0000000000 --- a/Loop/DefaultAssets.xcassets/presets.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.902", - "green" : "0.745", - "red" : "0.365" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.902", - "green" : "0.745", - "red" : "0.365" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Loop/DefaultAssets.xcassets/presets.imageset/Contents.json b/Loop/DefaultAssets.xcassets/presets.imageset/Contents.json deleted file mode 100644 index 217a8827cd..0000000000 --- a/Loop/DefaultAssets.xcassets/presets.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "presets.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/Loop/DefaultAssets.xcassets/presets.imageset/presets.pdf b/Loop/DefaultAssets.xcassets/presets.imageset/presets.pdf deleted file mode 100644 index 2a861e6e81661c31b48f5417c60013b1d429a5ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3000 zcmeH}!EV$r5Qgu5in&y34^3h_u@kAPM7I@+eO<1k`xu5zGKIkxSI;G zNZjhf%KOEc8P9J|=3se#am*vl7>5L!uiqHJ$qAgEGE=YZUnW8xzc=+we+v@f%C74A z)|7WFJ~ux%m07(xgT-a|uj<$j#-qdyNXE%A&FGnkk|HUj29ZWuXi;QP-7#7>pLA6{ zJ?0UY*f6Y*zxn_D-vrO%3hGec8!ntM$LoJ+Uc+7U>!8qocA)IR_ z@Gy{OKG&gSgaXBg1%eUtlkuRUeIkQiF19)Q7rlmyn7WIQV+w=ZlLMMObH!fdn#38d z-v5&;cZE>n@x|h3NB$Rj;jy-ZgB&IruNN__^hJ`Z1XLuFf=ev!gJoK-CSs4TU7i&a zqoYBVh>*Xaw0JiXFR!=cLlI+m3W!vUq9R6-lO-a7zdg$qSN2 zpp&4n9`2b~IyUiLy1-pY4jD8l)J-*9l@pni9OIVEpOLl`DIh;1W zDEo5T+&q2tp*7bm0nLV8+h0&whV30j>E3GGe>Z^Ssx8aiC0b2gWa%8aD2J>Z7_P*W0!63wj;F8 K*}=i UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage(named: "favorite-foods")!, style: .plain, target: self, action: #selector(presentFavoriteFoodsScreen)) - item.accessibilityLabel = NSLocalizedString("Favorite Foods", comment: "The label of the favorite foods button") - - item.tintColor = UIColor.carbTintColor - - return item - } - - private func createPresetsButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.presetsImage(selected: selected)!, style: .plain, target: self, action: #selector(onPresetsTapped)) - item.accessibilityLabel = NSLocalizedString("Presets", comment: "The label of the presets mode toggle button") - - if selected { - item.accessibilityTraits.insert(.selected) - item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") - } else { - item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") - } - - item.tintColor = UIColor.presetTintColor - item.isEnabled = isEnabled - - return item - } private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(premealButtonTapped(_:))) @@ -1502,50 +1442,6 @@ final class StatusTableViewController: LoopChartsTableViewController { return item } - @objc func onPresetsTapped() { - if preMealMode == true { - deviceManager.loopManager.mutateSettings { settings in - settings.clearOverride(matching: .preMeal) - } - } - else if workoutMode == true { - deviceManager.loopManager.mutateSettings { settings in - settings.clearOverride() - } - } - else { - if FeatureFlags.sensitivityOverridesEnabled { - performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) - } else { - presentPresetAlertController() - } - } - } - - func presentPresetAlertController() { - guard workoutModeAllowed else { - presentPreMealModeAlertController() - return - } - guard preMealModeAllowed else { - presentWorkoutModeAlertController() - return - } - - let vc = UIAlertController(presetTypeSelectionHandler: { [self] presetType in - switch presetType { - case .preMeal: - self.presentPreMealModeAlertController() - case .legacyWorkout: - self.presentWorkoutModeAlertController() - default: - assertionFailure("Unknown preset selected from presetAlertController: \(presetType)") - } - }) - - present(vc, animated: true, completion: nil) - } - @IBAction func premealButtonTapped(_ sender: UIBarButtonItem) { togglePreMealMode(confirm: false) } @@ -1742,7 +1638,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func automaticDosingStatusChanged(_ automaticDosingEnabled: Bool) { - updatePreMealModeAvailability(automaticDosingEnabled: automaticDosingEnabled) + updatePresetModeAvailability(automaticDosingEnabled: automaticDosingEnabled) hudView?.loopCompletionHUD.loopIconClosed = automaticDosingEnabled hudView?.loopCompletionHUD.closedLoopDisallowedLocalizedDescription = deviceManager.closedLoopDisallowedLocalizedDescription } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index afdc404fcc..942d4675b8 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -26,6 +26,7 @@ public struct SettingsView: View { @State private var pumpChooserIsPresented: Bool = false @State private var cgmChooserIsPresented: Bool = false + @State private var favoriteFoodsIsPresented: Bool = false @State private var serviceChooserIsPresented: Bool = false @State private var therapySettingsIsPresented: Bool = false @State private var deletePumpDataAlertIsPresented = false @@ -55,6 +56,7 @@ public struct SettingsView: View { configurationSection } deviceSettingsSection + favoriteFoodsSection if (viewModel.pumpManagerSettingsViewModel.isTestingDevice || viewModel.cgmManagerSettingsViewModel.isTestingDevice) && viewModel.showDeleteTestData { deleteDataSection } @@ -298,6 +300,19 @@ extension SettingsView { } } + private var favoriteFoodsSection: some View { + Section { + LargeButton(action: { self.favoriteFoodsIsPresented = true }, + includeArrow: true, + imageView: AnyView(Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor)), + label: "Favorite Foods", + descriptiveText: "Simplify Carb Entry") + } + .sheet(isPresented: $favoriteFoodsIsPresented) { + FavoriteFoodsView() + } + } + private var cgmChoices: [ActionSheet.Button] { var result = viewModel.cgmManagerSettingsViewModel.availableDevices .sorted(by: {$0.localizedTitle < $1.localizedTitle}) From 86404f73a784358aba85731c91c469cf52dd5fe1 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Wed, 9 Aug 2023 20:42:44 -0400 Subject: [PATCH 10/22] [LOOP-4699] StoredFavoriteFood, main screen design updates --- Loop.xcodeproj/project.pbxproj | 4 + Loop/Extensions/UserDefaults+Loop.swift | 18 +- .../CarbEntryViewController.swift | 10 - .../StatusTableViewController.swift | 8 - Loop/View Models/FavoriteFoodsViewModel.swift | 73 ++++++ Loop/Views/FavoriteFoodsView.swift | 218 +++++------------- 6 files changed, 148 insertions(+), 183 deletions(-) create mode 100644 Loop/View Models/FavoriteFoodsViewModel.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 04c5983620..9c54dc94b3 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 14B1737F28AEDC6C006CCD7C /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* PluginManager.swift */; }; 14B1738028AEDC6C006CCD7C /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D58222F0A27008961AB /* Debug.swift */; }; 14B1738128AEDC70006CCD7C /* StatusExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */; }; + 14D906F42A846510006EB79A /* FavoriteFoodsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */; }; 1D05219B2469E9DF000EBBDE /* StoredAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219A2469E9DF000EBBDE /* StoredAlert.swift */; }; 1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219C2469F1F5000EBBDE /* AlertStore.swift */; }; 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */; }; @@ -742,6 +743,7 @@ 14B1736F28AEDBF6006CCD7C /* SystemStatusWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemStatusWidget.swift; sourceTree = ""; }; 14B1737028AEDBF6006CCD7C /* GlucoseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseView.swift; sourceTree = ""; }; 14B1737128AEDBF6006CCD7C /* LoopCircleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopCircleView.swift; sourceTree = ""; }; + 14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsViewModel.swift; sourceTree = ""; }; 1D05219A2469E9DF000EBBDE /* StoredAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredAlert.swift; sourceTree = ""; }; 1D05219C2469F1F5000EBBDE /* AlertStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStore.swift; sourceTree = ""; }; 1D080CBC2473214A00356610 /* AlertStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AlertStore.xcdatamodel; sourceTree = ""; }; @@ -2583,6 +2585,7 @@ children = ( 897A5A9824C22DE800C4E71D /* BolusEntryViewModel.swift */, A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */, + 14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */, C11BD0542523CFED00236B08 /* SimpleBolusViewModel.swift */, 1DB1CA4E24A56D7600B3B94C /* SettingsViewModel.swift */, 1D49795724E7289700948F05 /* ServicesViewModel.swift */, @@ -3769,6 +3772,7 @@ 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */, 1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */, 439897371CD2F80600223065 /* AnalyticsServicesManager.swift in Sources */, + 14D906F42A846510006EB79A /* FavoriteFoodsViewModel.swift in Sources */, DDC389FE2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift in Sources */, A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */, 895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */, diff --git a/Loop/Extensions/UserDefaults+Loop.swift b/Loop/Extensions/UserDefaults+Loop.swift index f3bfe41293..4894dcc777 100644 --- a/Loop/Extensions/UserDefaults+Loop.swift +++ b/Loop/Extensions/UserDefaults+Loop.swift @@ -16,7 +16,7 @@ extension UserDefaults { case legacyServicesState = "com.loopkit.Loop.ServicesState" case loopNotRunningNotifications = "com.loopkit.Loop.loopNotRunningNotifications" case inFlightAutomaticDose = "com.loopkit.Loop.inFlightAutomaticDose" - case favoriteFoodsEnabled = "com.loopkit.Loop.favoriteFoodsEnabled" + case favoriteFoods = "com.loopkit.Loop.favoriteFoods" } var legacyPumpManagerRawValue: PumpManager.RawValue? { @@ -91,12 +91,22 @@ extension UserDefaults { } } - var favoriteFoodsEnabled: Bool { + var favoriteFoods: [StoredFavoriteFood] { get { - bool(forKey: Key.favoriteFoodsEnabled.rawValue) + let decoder = JSONDecoder() + guard let data = object(forKey: Key.favoriteFoods.rawValue) as? Data else { + return [] + } + return (try? decoder.decode([StoredFavoriteFood].self, from: data)) ?? [] } set { - set(newValue, forKey: Key.favoriteFoodsEnabled.rawValue) + do { + let encoder = JSONEncoder() + let data = try encoder.encode(newValue) + set(data, forKey: Key.favoriteFoods.rawValue) + } catch { + assertionFailure("Unable to encode stored favorite foods") + } } } } diff --git a/Loop/View Controllers/CarbEntryViewController.swift b/Loop/View Controllers/CarbEntryViewController.swift index 1e7095ee07..92b7ef7be4 100644 --- a/Loop/View Controllers/CarbEntryViewController.swift +++ b/Loop/View Controllers/CarbEntryViewController.swift @@ -173,8 +173,6 @@ final class CarbEntryViewController: LoopChartsTableViewController, Identifiable }() private var lastContentHeight: CGFloat = 0 - - public weak var favoriteFoodsDelegate: FavoriteFoodsFeatureUnlockDelegate? override func createChartsManager() -> ChartsManager { // Consider including a chart on this screen to demonstrate how absorption time affects prediction @@ -554,10 +552,6 @@ final class CarbEntryViewController: LoopChartsTableViewController, Identifiable guard let updatedEntry = updatedCarbEntry else { return } - - if foodType == "🍞🥜🍫🥛" && quantity?.doubleValue(for: .gram()) == 63 { - favoriteFoodsDelegate?.featureAvailabilityChanged() - } let viewModel = BolusEntryViewModel( delegate: deviceManager, @@ -790,7 +784,3 @@ extension CarbEntryViewController: EmojiInputControllerDelegate { extension DateAndDurationTableViewCell: NibLoadable {} extension DateAndDurationSteppableTableViewCell: NibLoadable {} - -protocol FavoriteFoodsFeatureUnlockDelegate: AnyObject { - func featureAvailabilityChanged() -} diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 954a08e31a..a9006b43fd 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1370,7 +1370,6 @@ final class StatusTableViewController: LoopChartsTableViewController { hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) } else { let carbEntryViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "CarbEntryViewController") as! CarbEntryViewController - carbEntryViewController.favoriteFoodsDelegate = self carbEntryViewController.deviceManager = deviceManager carbEntryViewController.defaultAbsorptionTimes = deviceManager.carbStore.defaultAbsorptionTimes carbEntryViewController.preferredCarbUnit = deviceManager.carbStore.preferredUnit @@ -2245,10 +2244,3 @@ extension StatusTableViewController: ServicesViewModelDelegate { show(settingsViewController, sender: self) } } - -extension StatusTableViewController: FavoriteFoodsFeatureUnlockDelegate { - func featureAvailabilityChanged() { - UserDefaults.appGroup?.favoriteFoodsEnabled.toggle() - self.updateToolbarItems() - } -} diff --git a/Loop/View Models/FavoriteFoodsViewModel.swift b/Loop/View Models/FavoriteFoodsViewModel.swift new file mode 100644 index 0000000000..3138e097dc --- /dev/null +++ b/Loop/View Models/FavoriteFoodsViewModel.swift @@ -0,0 +1,73 @@ +// +// FavoriteFoodsViewModel.swift +// Loop +// +// Created by Noah Brauner on 7/27/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import Combine + +final class FavoriteFoodsViewModel: ObservableObject { + @Published var favoriteFoods = UserDefaults.standard.favoriteFoods + @Published var selectedFood: StoredFavoriteFood? + + @Published var isDetailViewActive = false + @Published var isEditViewActive = false + @Published var isAddViewActive = false + + private lazy var cancellables = Set() + + init() { + observeFavoriteFoodChange() + } + + func onFoodSave(_ newFood: NewFavoriteFood) { + if isAddViewActive { + let newStoredFood = StoredFavoriteFood(name: newFood.name, carbsQuantity: newFood.carbsQuantity, foodType: newFood.foodType, absorptionTime: newFood.absorptionTime) + withAnimation { + favoriteFoods.append(newStoredFood) + } + isAddViewActive = false + } + else if var selectedFood, let selectedFooxIndex = favoriteFoods.firstIndex(of: selectedFood) { + selectedFood.name = newFood.name + selectedFood.carbsQuantity = newFood.carbsQuantity + selectedFood.foodType = newFood.foodType + selectedFood.absorptionTime = newFood.absorptionTime + favoriteFoods[selectedFooxIndex] = selectedFood + isEditViewActive = false + } + } + + func onFoodDelete(_ food: StoredFavoriteFood) { + if isDetailViewActive { + isDetailViewActive = false + } + withAnimation { + _ = favoriteFoods.remove(food) + } + } + + func onFoodReorder(from: IndexSet, to: Int) { + withAnimation { + favoriteFoods.move(fromOffsets: from, toOffset: to) + } + } + + func addFoodTapped() { + isAddViewActive = true + } + + private func observeFavoriteFoodChange() { + $favoriteFoods + .dropFirst() + .removeDuplicates() + .sink { newValue in + UserDefaults.standard.favoriteFoods = newValue + } + .store(in: &cancellables) + } +} diff --git a/Loop/Views/FavoriteFoodsView.swift b/Loop/Views/FavoriteFoodsView.swift index 6abb06848a..be67493509 100644 --- a/Loop/Views/FavoriteFoodsView.swift +++ b/Loop/Views/FavoriteFoodsView.swift @@ -9,72 +9,62 @@ import SwiftUI import LoopKit import LoopKitUI -import HealthKit struct FavoriteFoodsView: View { - @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference @Environment(\.dismissAction) private var dismiss - @Environment(\.carbTintColor) private var carbTintColor + + @StateObject private var viewModel = FavoriteFoodsViewModel() @State private var foodToConfirmDeleteId: String? = nil @State private var editMode: EditMode = .inactive - - @State private var foods = allFoods - - @State var isBolusViewActive = false - @State var isEditViewActive = false - @State var isAddViewActive = false - - @State var selectedFood: FavoriteFood? = nil - - @State private var draggingFood: FavoriteFood? - @State private var hasChangedLocation: Bool = false var body: some View { - NavigationViewWrappedContent { - ScrollView { - VStack(spacing: 16) { - VStack(spacing: 10) { - HStack { - Text("All Favorites") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - - Spacer() - - editButton + NavigationView { + VStack { + List { + if viewModel.favoriteFoods.isEmpty { + Section { + Text("Selecting a favorite food in the carb entry screen automatically fills in the carb quantity, food type, and absorption time fields! Tap the add button below to create your first favorite food!") } - - ForEach(foods) { food in - draggableFoodCardView(food: food) + } + else { + Section(header: listHeader) { + ForEach(viewModel.favoriteFoods) { food in + FavoriteFoodListRow(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: viewModel.onFoodDelete(_:)) + .environment(\.editMode, self.$editMode) + .listRowInsets(EdgeInsets()) + } + .onMove(perform: viewModel.onFoodReorder(from:to:)) + .moveDisabled(!editMode.isEditing) + .deleteDisabled(true) } } - .environment(\.editMode, self.$editMode) - newFoodButton + Section { + addFoodButton + .listRowInsets(EdgeInsets()) + } + } + .insetGroupedListStyle() + + + NavigationLink(destination: Text("Edit View"), isActive: $viewModel.isEditViewActive) { + EmptyView() + } + + NavigationLink(destination: Text("Detail View"), isActive: $viewModel.isDetailViewActive) { + EmptyView() } - .padding() } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { dismissButton } } - .navigationBarTitle(Text(NSLocalizedString("Favorite Foods", comment: "Favorite Foods screen title"))) - .navigationViewStyle(.stack) - - NavigationLink(destination: Text("Coming later:\nprepopulated carb entry screen").multilineTextAlignment(.center), isActive: $isBolusViewActive) { - EmptyView() - } - - NavigationLink(destination: Text("Coming later:\nedit favorite food screen").multilineTextAlignment(.center), isActive: $isEditViewActive) { - EmptyView() - } - - NavigationLink(destination: Text("Coming later:\nadd favorite food screen").multilineTextAlignment(.center), isActive: $isAddViewActive) { - EmptyView() - } + .navigationBarTitle("Favorite Foods", displayMode: .large) + } + .sheet(isPresented: $viewModel.isAddViewActive) { + Text("Add View") } .onChange(of: editMode) { newValue in if !newValue.isEditing { @@ -83,71 +73,38 @@ struct FavoriteFoodsView: View { } } - private func addFood() { - isAddViewActive = true - } - - private func onFoodTap(_ food: FavoriteFood) { - selectedFood = food + private func onFoodTap(_ food: StoredFavoriteFood) { + viewModel.selectedFood = food if editMode.isEditing { - isEditViewActive = true + viewModel.isEditViewActive = true } else { - isBolusViewActive = true - } - } - private func onFoodDelete(_ food: FavoriteFood) { - withAnimation(.easeInOut(duration: 0.3)) { - _ = foods.remove(food) - } - } - - private func onFoodReorder(from: IndexSet, to: Int) { - withAnimation { - foods.move(fromOffsets: from, toOffset: to) + viewModel.isDetailViewActive = true } } } extension FavoriteFoodsView { - @ViewBuilder func draggableFoodCardView(food: FavoriteFood) -> some View { - Button(action: { - onFoodTap(food) - }) { - FavoriteFoodCardView(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: onFoodDelete(_:)) - .onDrag { - draggingFood = food - return NSItemProvider(object: "\(food.id)" as NSString) - } preview: { - FavoriteFoodCardView(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: onFoodDelete(_:)) - } - .onDrop( - of: [UTType.text], - delegate: DragRelocateDelegate( - item: food, - listData: foods, - current: $draggingFood, - hasChangedLocation: $hasChangedLocation - ) { from, to in - onFoodReorder(from: from, to: to) - } - ) - .disabled(!editMode.isEditing) - .buttonStyle(ListButtonStyle()) + private var listHeader: some View { + HStack { + Text("All Favorites") + .font(.title3) + .fontWeight(.semibold) + .textCase(nil) + .foregroundColor(.primary) + + Spacer() + + editButton } + .listRowInsets(EdgeInsets(top: 20, leading: 4, bottom: 10, trailing: 4)) } private var dismissButton: some View { Button(action: dismiss) { - Text("Cancel") + Text("Done") } } - -// private var plusButton: some View { -// Button(action: addFood) { -// Image(systemName: "plus") -// } -// } private var editButton: some View { Button(action: { @@ -156,11 +113,12 @@ extension FavoriteFoodsView { } }) { Text(editMode.title) + .textCase(nil) } } - private var newFoodButton: some View { - Button(action: addFood) { + private var addFoodButton: some View { + Button(action: viewModel.addFoodTapped) { HStack { Image(systemName: "plus.circle.fill") @@ -168,67 +126,5 @@ extension FavoriteFoodsView { } } .buttonStyle(ActionButtonStyle()) - .padding(.top) - } -} - -fileprivate struct NavigationViewWrappedContent: View { - var content: Content - - init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - var body: some View { - NavigationView { - ZStack { - Color(.systemGroupedBackground) - .edgesIgnoringSafeArea(.all) - - content - } - } } } - -fileprivate struct DragRelocateDelegate: DropDelegate { - let item: Item - var listData: [Item] - @Binding var current: Item? - @Binding var hasChangedLocation: Bool - - var moveAction: (IndexSet, Int) -> Void - - func dropEntered(info: DropInfo) { - guard item != current, let current = current else { return } - guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return } - - hasChangedLocation = true - - if listData[to] != current { - moveAction(IndexSet(integer: from), to > from ? to + 1 : to) - } - } - - func dropUpdated(info: DropInfo) -> DropProposal? { - DropProposal(operation: .move) - } - - func performDrop(info: DropInfo) -> Bool { - hasChangedLocation = false - current = nil - return true - } -} - -fileprivate let allFoods = [ - // Some really yummy foods... - FavoriteFood(carbsQuantity: carbs(55), foodType: "🥞🥚", absorptionTime: .hours(3), name: "Pancakes and Eggs"), - FavoriteFood(carbsQuantity: carbs(35), foodType: "🍌🍞", absorptionTime: .hours(2), name: "Banana Bread"), - FavoriteFood(carbsQuantity: carbs(63), foodType: "🍞🥜🍫🥛", absorptionTime: .hours(3), name: "The Best Lunch"), - FavoriteFood(carbsQuantity: carbs(120), foodType: "🍕", absorptionTime: .hours(5), name: "Dad's Pizza"), -] - -fileprivate func carbs(_ value: Double) -> HKQuantity { - return HKQuantity(unit: .gram(), doubleValue: value) -} From 04980c5cb233f9e13b7166ecb77a2d9b22d9edc3 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Thu, 10 Aug 2023 10:22:19 -0400 Subject: [PATCH 11/22] [LOOP-4690] Add/Edit/Detail for favorite foods --- Loop.xcodeproj/project.pbxproj | 12 ++ .../AddEditFavoriteFoodViewModel.swift | 109 ++++++++++++ Loop/Views/AddEditFavoriteFoodView.swift | 168 ++++++++++++++++++ Loop/Views/FavoriteFoodsView.swift | 6 +- Loop/Views/HowAbsorptionTimeWorksView.swift | 33 ++++ 5 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 Loop/View Models/AddEditFavoriteFoodViewModel.swift create mode 100644 Loop/Views/AddEditFavoriteFoodView.swift create mode 100644 Loop/Views/HowAbsorptionTimeWorksView.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 9c54dc94b3..92312d404d 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; }; 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB7582A60BF2E0075748A /* EditMode.swift */; }; 142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */; }; + 1452F4A92A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */; }; + 1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */; }; + 1452F4AD2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; @@ -735,6 +738,9 @@ 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = ""; }; 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SmallStatusWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodViewModel.swift; sourceTree = ""; }; + 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodView.swift; sourceTree = ""; }; + 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowAbsorptionTimeWorksView.swift; sourceTree = ""; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 14B1736428AED9EE006CCD7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2215,6 +2221,7 @@ 43F5C2CF1B92A2ED003EB13D /* Views */ = { isa = PBXGroup; children = ( + 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */, B4001CED28CBBC82002FB414 /* AlertManagementView.swift */, 897A5A9524C2175B00C4E71D /* BolusEntryView.swift */, C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */, @@ -2225,6 +2232,7 @@ C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */, 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */, 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */, + 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */, B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */, 430D85881F44037000AF2D4F /* HUDViewTableViewCell.swift */, A91D2A3E26CF0FF80023B075 /* IconTitleSubtitleTableViewCell.swift */, @@ -2583,6 +2591,7 @@ 897A5A9724C22DCE00C4E71D /* View Models */ = { isa = PBXGroup; children = ( + 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */, 897A5A9824C22DE800C4E71D /* BolusEntryViewModel.swift */, A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */, 14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */, @@ -3631,6 +3640,7 @@ 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, E9BB27AB23B85C3500FB4987 /* SleepStore.swift in Sources */, C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */, + 1452F4AD2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift in Sources */, C16575712538A36B004AE16E /* CGMStalenessMonitor.swift in Sources */, 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */, C11BD0552523CFED00236B08 /* SimpleBolusViewModel.swift in Sources */, @@ -3648,6 +3658,7 @@ 142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */, A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, + 1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */, C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */, 43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */, 43FCEEA9221A615B0013DD30 /* StatusChartsManager.swift in Sources */, @@ -3782,6 +3793,7 @@ 430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */, 43A51E211EB6DBDD000736CC /* LoopChartsTableViewController.swift in Sources */, 8968B1122408B3520074BB48 /* UIFont.swift in Sources */, + 1452F4A92A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift in Sources */, 438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */, 89A1B66E24ABFDF800117AC2 /* SupportedBolusVolumesUserInfo.swift in Sources */, C1C660D1252E4DD5009B5C32 /* LoopConstants.swift in Sources */, diff --git a/Loop/View Models/AddEditFavoriteFoodViewModel.swift b/Loop/View Models/AddEditFavoriteFoodViewModel.swift new file mode 100644 index 0000000000..5bd6eb8775 --- /dev/null +++ b/Loop/View Models/AddEditFavoriteFoodViewModel.swift @@ -0,0 +1,109 @@ +// +// AddEditFavoriteFoodViewModel.swift +// Loop +// +// Created by Noah Brauner on 7/31/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import HealthKit + +final class AddEditFavoriteFoodViewModel: ObservableObject { + enum Alert: Identifiable { + var id: Self { + return self + } + + case maxQuantityExceded + case warningQuantityValidation + } + + @Published var name = "" + + @Published var carbsQuantity: Double? = nil + var preferredCarbUnit = HKUnit.gram() + var maxCarbEntryQuantity = LoopConstants.maxCarbEntryQuantity + var warningCarbEntryQuantity = LoopConstants.warningCarbEntryQuantity + + @Published var foodType = "" + + @Published var absorptionTime: TimeInterval + let minAbsorptionTime = LoopConstants.minCarbAbsorptionTime + let maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime + var absorptionRimesRange: ClosedRange { + return minAbsorptionTime...maxAbsorptionTime + } + + @Published var alert: AddEditFavoriteFoodViewModel.Alert? + + private let onSave: (NewFavoriteFood) -> () + + init(originalFavoriteFood: StoredFavoriteFood?, onSave: @escaping (NewFavoriteFood) -> ()) { + self.onSave = onSave + if let food = originalFavoriteFood { + self.originalFavoriteFood = food + self.name = food.name + self.carbsQuantity = food.carbsQuantity.doubleValue(for: preferredCarbUnit) + self.foodType = food.foodType + self.absorptionTime = food.absorptionTime + } + else { + self.absorptionTime = .hours(3) + } + } + + init(carbsQuantity: Double?, foodType: String, absorptionTime: TimeInterval, onSave: @escaping (NewFavoriteFood) -> ()) { + self.onSave = onSave + self.carbsQuantity = carbsQuantity + self.foodType = foodType + self.absorptionTime = absorptionTime + } + + var originalFavoriteFood: StoredFavoriteFood? + var updatedFavoriteFood: NewFavoriteFood? { + if let quantity = carbsQuantity, quantity != 0, name != "", foodType != "" { + if let o = originalFavoriteFood, o.name == name, o.carbsQuantity.doubleValue(for: preferredCarbUnit) == carbsQuantity && o.foodType == foodType && o.absorptionTime == absorptionTime { + return nil // No changes were made + } + + return NewFavoriteFood( + name: name, + carbsQuantity: HKQuantity(unit: preferredCarbUnit, doubleValue: quantity), + foodType: foodType, + absorptionTime: absorptionTime + ) + } + else { + return nil + } + } + + func save() { + guard let updatedFavoriteFood, absorptionTime <= maxAbsorptionTime else { return } + + guard let carbsQuantity, carbsQuantity > 0 else { return } + let quantity = HKQuantity(unit: preferredCarbUnit, doubleValue: carbsQuantity) + if quantity.compare(maxCarbEntryQuantity) == .orderedDescending { + self.alert = .maxQuantityExceded + return + } + else if quantity.compare(warningCarbEntryQuantity) == .orderedDescending { + self.alert = .warningQuantityValidation + return + } + + onSave(updatedFavoriteFood) + } + + func clearAlertAndSave() { + guard let updatedFavoriteFood else { return } + self.alert = nil + onSave(updatedFavoriteFood) + } + + func clearAlert() { + self.alert = nil + } +} diff --git a/Loop/Views/AddEditFavoriteFoodView.swift b/Loop/Views/AddEditFavoriteFoodView.swift new file mode 100644 index 0000000000..05ee94c551 --- /dev/null +++ b/Loop/Views/AddEditFavoriteFoodView.swift @@ -0,0 +1,168 @@ +// +// AddEditFavoriteFoodView.swift +// Loop +// +// Created by Noah Brauner on 7/31/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import LoopKitUI + +struct AddEditFavoriteFoodView: View { + @Environment(\.dismiss) var dismiss + + @StateObject private var viewModel: AddEditFavoriteFoodViewModel + + @State private var expandedRow: Row? + @State private var showHowAbsorptionTimeWorks = false + + private var isNewEntry = true + + /// Initializer for adding a new favorite food or editing a `StoredFavoriteFood` + init(originalFavoriteFood: StoredFavoriteFood? = nil, onSave: @escaping (NewFavoriteFood) -> Void) { + self._viewModel = StateObject(wrappedValue: AddEditFavoriteFoodViewModel(originalFavoriteFood: originalFavoriteFood, onSave: onSave)) + self.isNewEntry = originalFavoriteFood == nil + } + + /// Initializer for presenting the `AddEditFavoriteFoodView` prepopulated from the `CarbEntryView` + init(carbsQuantity: Double?, foodType: String, absorptionTime: TimeInterval, onSave: @escaping (NewFavoriteFood) -> Void) { + self._viewModel = StateObject(wrappedValue: AddEditFavoriteFoodViewModel(carbsQuantity: carbsQuantity, foodType: foodType, absorptionTime: absorptionTime, onSave: onSave)) + } + + var body: some View { + if isNewEntry { + NavigationView { + content + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + dismissButton + } + + ToolbarItem(placement: .navigationBarTrailing) { + saveButton + } + } + .navigationBarTitle("New Favorite Food", displayMode: .inline) + .onAppear { + expandedRow = .name + } + } + } + else { + content + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + if viewModel.updatedFavoriteFood != nil { + dismissButton + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + saveButton + } + } + .navigationBarBackButtonHidden(viewModel.updatedFavoriteFood != nil) + .navigationBarTitle(viewModel.originalFavoriteFood?.title ?? "", displayMode: .inline) + } + } + + private var content: some View { + ZStack { + Color(.systemGroupedBackground) + .edgesIgnoringSafeArea(.all) + + ScrollView { + card + .padding(.top, 8) + + saveActionButton + } + } + .alert(item: $viewModel.alert, content: alert(for:)) + .sheet(isPresented: $showHowAbsorptionTimeWorks) { + HowAbsorptionTimeWorksView() + } + } + + private var card: some View { + VStack(spacing: 10) { + TextFieldRow(text: $viewModel.name, title: "Name", placeholder: "Apple", expandedRow: $expandedRow, row: .name) + + CardSectionDivider() + + CarbQuantityRow(quantity: $viewModel.carbsQuantity, title: "Carb Quantity", preferredCarbUnit: viewModel.preferredCarbUnit, expandedRow: $expandedRow, row: Row.amountConsumed) + + CardSectionDivider() + + EmojiRow(emojiType: .food, text: $viewModel.foodType, title: "Food Type", expandedRow: $expandedRow, row: .foodType) + + CardSectionDivider() + + AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, validDurationRange: viewModel.absorptionRimesRange, expandedRow: $expandedRow, row: Row.absorptionTime, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) + .padding(.bottom, 2) + } + .padding(.vertical, 12) + .padding(.horizontal) + .background(CardBackground()) + .padding(.horizontal) + } + + private func alert(for alert: AddEditFavoriteFoodViewModel.Alert) -> SwiftUI.Alert { + switch alert { + case .maxQuantityExceded: + let message = String( + format: NSLocalizedString("The maximum allowed amount is %@ grams.", comment: "Alert body displayed for quantity greater than max (1: maximum quantity in grams)"), + NumberFormatter.localizedString(from: NSNumber(value: viewModel.maxCarbEntryQuantity.doubleValue(for: viewModel.preferredCarbUnit)), number: .none) + ) + let okMessage = NSLocalizedString("com.loudnate.LoopKit.errorAlertActionTitle", value: "OK", comment: "The title of the action used to dismiss an error alert") + return SwiftUI.Alert( + title: Text("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered"), + message: Text(message), + dismissButton: .cancel(Text(okMessage), action: viewModel.clearAlert) + ) + case .warningQuantityValidation: + let message = String( + format: NSLocalizedString("Did you intend to enter %1$@ grams as the amount of carbohydrates for this meal?", comment: "Alert body when entered carbohydrates is greater than threshold (1: entered quantity in grams)"), + NumberFormatter.localizedString(from: NSNumber(value: viewModel.carbsQuantity ?? 0), number: .none) + ) + return SwiftUI.Alert( + title: Text("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered"), + message: Text(message), + primaryButton: .default(Text("No, edit amount", comment: "The title of the action used when rejecting the the amount of carbohydrates entered."), action: viewModel.clearAlert), + secondaryButton: .cancel(Text("Yes", comment: "The title of the action used when confirming entered amount of carbohydrates."), action: viewModel.clearAlertAndSave) + ) + } + } +} + +extension AddEditFavoriteFoodView { + private var dismissButton: some View { + Button(action: dismiss.callAsFunction) { + Text("Cancel") + } + } + + private var saveActionButton: some View { + Button(action: viewModel.save) { + Text("Save") + } + .buttonStyle(ActionButtonStyle()) + .padding() + .disabled(viewModel.updatedFavoriteFood == nil) + } + + private var saveButton: some View { + Button(action: viewModel.save) { + Text("Save") + } + .disabled(viewModel.updatedFavoriteFood == nil) + } +} + +extension AddEditFavoriteFoodView { + enum Row { + case name, amountConsumed, foodType, absorptionTime + } +} diff --git a/Loop/Views/FavoriteFoodsView.swift b/Loop/Views/FavoriteFoodsView.swift index be67493509..5108b17d3b 100644 --- a/Loop/Views/FavoriteFoodsView.swift +++ b/Loop/Views/FavoriteFoodsView.swift @@ -48,11 +48,11 @@ struct FavoriteFoodsView: View { .insetGroupedListStyle() - NavigationLink(destination: Text("Edit View"), isActive: $viewModel.isEditViewActive) { + NavigationLink(destination: AddEditFavoriteFoodView(originalFavoriteFood: viewModel.selectedFood, onSave: viewModel.onFoodSave(_:)), isActive: $viewModel.isEditViewActive) { EmptyView() } - NavigationLink(destination: Text("Detail View"), isActive: $viewModel.isDetailViewActive) { + NavigationLink(destination: FavoriteFoodDetailView(food: viewModel.selectedFood, onFoodDelete: viewModel.onFoodDelete(_:)), isActive: $viewModel.isDetailViewActive) { EmptyView() } } @@ -64,7 +64,7 @@ struct FavoriteFoodsView: View { .navigationBarTitle("Favorite Foods", displayMode: .large) } .sheet(isPresented: $viewModel.isAddViewActive) { - Text("Add View") + AddEditFavoriteFoodView(onSave: viewModel.onFoodSave(_:)) } .onChange(of: editMode) { newValue in if !newValue.isEditing { diff --git a/Loop/Views/HowAbsorptionTimeWorksView.swift b/Loop/Views/HowAbsorptionTimeWorksView.swift new file mode 100644 index 0000000000..7ac01080e2 --- /dev/null +++ b/Loop/Views/HowAbsorptionTimeWorksView.swift @@ -0,0 +1,33 @@ +// +// HowAbsorptionTimeWorksView.swift +// Loop +// +// Created by Noah Brauner on 7/28/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +struct HowAbsorptionTimeWorksView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationView { + List { + Section { + Text("Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact.", comment: "Carb entry section footer text explaining absorption time") + } + } + .navigationTitle("Absorption Time") + .toolbar { + dismissButton + } + } + } + + private var dismissButton: some View { + Button(action: dismiss.callAsFunction) { + Text("Close") + } + } +} From c25ca70a47fc3316e886add75b55651f81029c7a Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Thu, 10 Aug 2023 12:32:42 -0400 Subject: [PATCH 12/22] [LOOP-4684] Carb Entry SwiftUI Refactor --- Loop.xcodeproj/project.pbxproj | 8 + Loop/Base.lproj/Main.storyboard | 202 +----------------- ...aManager+BolusEntryViewModelDelegate.swift | 6 + Loop/Extensions/UIAlertController.swift | 24 --- .../CarbAbsorptionViewController.swift | 71 +++--- .../StatusTableViewController.swift | 15 +- Loop/View Models/BolusEntryViewModel.swift | 2 +- Loop/View Models/CarbEntryViewModel.swift | 194 +++++++++++++++++ Loop/Views/CarbEntryView.swift | 180 ++++++++++++++++ 9 files changed, 428 insertions(+), 274 deletions(-) create mode 100644 Loop/View Models/CarbEntryViewModel.swift create mode 100644 Loop/Views/CarbEntryView.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 92312d404d..a24c173993 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */; }; 1452F4AD2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; + 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */; }; + 149A28BD2A853E6C00052EDF /* CarbEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; 14B1736528AED9EE006CCD7C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14B1736428AED9EE006CCD7C /* Assets.xcassets */; }; @@ -741,6 +743,8 @@ 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodViewModel.swift; sourceTree = ""; }; 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodView.swift; sourceTree = ""; }; 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowAbsorptionTimeWorksView.swift; sourceTree = ""; }; + 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; + 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 14B1736428AED9EE006CCD7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2227,6 +2231,7 @@ C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */, C1F8B1DB223862D500DD66CF /* BolusProgressTableViewCell.xib */, 43B260481ED248FB008CAA77 /* CarbEntryTableViewCell.swift */, + 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */, 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */, A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */, C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */, @@ -2593,6 +2598,7 @@ children = ( 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */, 897A5A9824C22DE800C4E71D /* BolusEntryViewModel.swift */, + 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */, A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */, 14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */, C11BD0542523CFED00236B08 /* SimpleBolusViewModel.swift */, @@ -3644,12 +3650,14 @@ C16575712538A36B004AE16E /* CGMStalenessMonitor.swift in Sources */, 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */, C11BD0552523CFED00236B08 /* SimpleBolusViewModel.swift in Sources */, + 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */, C19008FE25225D3900721625 /* SimpleBolusCalculator.swift in Sources */, C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */, 89D6953E23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift in Sources */, 89CA2B30226C0161004D9350 /* DirectoryObserver.swift in Sources */, 1DA649A7244126CD00F61E75 /* UserNotificationAlertScheduler.swift in Sources */, 439A7942211F631C0041B75F /* RootNavigationController.swift in Sources */, + 149A28BD2A853E6C00052EDF /* CarbEntryView.swift in Sources */, 4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */, 89E267FC2292456700A3F2AF /* FeatureFlags.swift in Sources */, 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */, diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index d9812eb13a..dac14ecfb4 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -447,7 +447,6 @@ - @@ -693,206 +692,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/Loop/Extensions/DeviceDataManager+BolusEntryViewModelDelegate.swift b/Loop/Extensions/DeviceDataManager+BolusEntryViewModelDelegate.swift index cb92136105..25173f92d8 100644 --- a/Loop/Extensions/DeviceDataManager+BolusEntryViewModelDelegate.swift +++ b/Loop/Extensions/DeviceDataManager+BolusEntryViewModelDelegate.swift @@ -10,6 +10,12 @@ import HealthKit import LoopCore import LoopKit +extension DeviceDataManager: CarbEntryViewModelDelegate { + var defaultAbsorptionTimes: LoopKit.CarbStore.DefaultAbsorptionTimes { + return carbStore.defaultAbsorptionTimes + } +} + extension DeviceDataManager: BolusEntryViewModelDelegate, ManualDoseViewModelDelegate { func addManuallyEnteredDose(startDate: Date, units: Double, insulinType: InsulinType?) { diff --git a/Loop/Extensions/UIAlertController.swift b/Loop/Extensions/UIAlertController.swift index 428d13e0d9..3b83aa11d8 100644 --- a/Loop/Extensions/UIAlertController.swift +++ b/Loop/Extensions/UIAlertController.swift @@ -12,30 +12,6 @@ import LoopKitUI extension UIAlertController { - /** - Initializes an ActionSheet-styled controller for selecting a preset type - - - parameter handler: A closure to execute when the sheet is dismissed after selection. The closure has a single argument: - - duration: The duration for which the workout is to be enabled - */ - internal convenience init(presetTypeSelectionHandler handler: @escaping (_ selection: TemporaryScheduleOverride.Context) -> Void) { - self.init( - title: NSLocalizedString("Choose a Preset", comment: "The title of the alert controller used to select a preset"), - message: nil, - preferredStyle: .actionSheet - ) - - addAction(UIAlertAction(title: "Workout Preset", style: .default) { _ in - handler(.legacyWorkout) - }) - - addAction(UIAlertAction(title: "Pre-Meal Preset", style: .default) { _ in - handler(.preMeal) - }) - - addCancelAction() - } - /** Initializes an ActionSheet-styled controller for selecting a workout duration diff --git a/Loop/View Controllers/CarbAbsorptionViewController.swift b/Loop/View Controllers/CarbAbsorptionViewController.swift index 91a240714a..fc770192e9 100644 --- a/Loop/View Controllers/CarbAbsorptionViewController.swift +++ b/Loop/View Controllers/CarbAbsorptionViewController.swift @@ -5,7 +5,7 @@ // Copyright © 2017 LoopKit Authors. All rights reserved. // -import UIKit +import SwiftUI import HealthKit import Intents import LoopCore @@ -488,55 +488,44 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif return (allowEditing && carbStatuses[indexPath.row].entry.createdByCurrentApp) ? indexPath : nil } } - - // MARK: - Navigation - - override func restoreUserActivityState(_ activity: NSUserActivity) { - switch activity.activityType { - case NSUserActivity.newCarbEntryActivityType: - performSegue(withIdentifier: CarbEntryViewController.className, sender: activity) - default: - break - } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.row < carbStatuses.count else { return } + tableView.deselectRow(at: indexPath, animated: true) + + let originalCarbEntry = carbStatuses[indexPath.row].entry + + let viewModel = CarbEntryViewModel(delegate: deviceManager, originalCarbEntry: originalCarbEntry) + let carbEntryView = CarbEntryView(viewModel: viewModel) + .environmentObject(deviceManager.displayGlucosePreference) + .environment(\.dismissAction, carbEditWasCanceled) + let hostingController = UIHostingController(rootView: carbEntryView) + hostingController.title = "Edit Carb Entry" + hostingController.navigationItem.largeTitleDisplayMode = .never + let leftBarButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(carbEditWasCanceled)) + hostingController.navigationItem.backBarButtonItem = leftBarButton + navigationController?.pushViewController(hostingController, animated: true) + } + + @objc func carbEditWasCanceled() { + navigationController?.popToViewController(self, animated: true) } + // MARK: - Navigation @IBAction func presentCarbEntryScreen() { - let navigationWrapper: UINavigationController if FeatureFlags.simpleBolusCalculatorEnabled && !automaticDosingStatus.automaticDosingEnabled { let viewModel = SimpleBolusViewModel(delegate: deviceManager, displayMealEntry: true) let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter)) let hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) - navigationWrapper = UINavigationController(rootViewController: hostingController) + let navigationWrapper = UINavigationController(rootViewController: hostingController) hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) + present(navigationWrapper, animated: true) } else { - let carbEntryViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "CarbEntryViewController") as! CarbEntryViewController - - carbEntryViewController.deviceManager = deviceManager - carbEntryViewController.defaultAbsorptionTimes = deviceManager.carbStore.defaultAbsorptionTimes - carbEntryViewController.preferredCarbUnit = deviceManager.carbStore.preferredUnit - navigationWrapper = UINavigationController(rootViewController: carbEntryViewController) + let viewModel = CarbEntryViewModel(delegate: deviceManager) + let carbEntryView = CarbEntryView(viewModel: viewModel) + .environmentObject(deviceManager.displayGlucosePreference) + let hostingController = DismissibleHostingController(rootView: carbEntryView, isModalInPresentation: false) + present(hostingController, animated: true) } - self.present(navigationWrapper, animated: true) } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - switch segue.destination { - case let vc as CarbEntryViewController: - if let selectedCell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: selectedCell), indexPath.row < carbStatuses.count { - vc.originalCarbEntry = carbStatuses[indexPath.row].entry - } else if let activity = sender as? NSUserActivity { - vc.restoreUserActivityState(activity) - } - - vc.deviceManager = deviceManager - vc.defaultAbsorptionTimes = deviceManager.carbStore.defaultAbsorptionTimes - vc.preferredCarbUnit = deviceManager.carbStore.preferredUnit - default: - break - } - } - - @IBAction func unwindFromEditing(_ segue: UIStoryboardSegue) {} } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index a9006b43fd..b4da816dbd 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1368,17 +1368,14 @@ final class StatusTableViewController: LoopChartsTableViewController { let hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) navigationWrapper = UINavigationController(rootViewController: hostingController) hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) + present(navigationWrapper, animated: true) } else { - let carbEntryViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "CarbEntryViewController") as! CarbEntryViewController - carbEntryViewController.deviceManager = deviceManager - carbEntryViewController.defaultAbsorptionTimes = deviceManager.carbStore.defaultAbsorptionTimes - carbEntryViewController.preferredCarbUnit = deviceManager.carbStore.preferredUnit - if let activity = activity { - carbEntryViewController.restoreUserActivityState(activity) - } - navigationWrapper = UINavigationController(rootViewController: carbEntryViewController) + let viewModel = CarbEntryViewModel(delegate: deviceManager) + let carbEntryView = CarbEntryView(viewModel: viewModel) + .environmentObject(deviceManager.displayGlucosePreference) + let hostingController = DismissibleHostingController(rootView: carbEntryView, isModalInPresentation: false) + present(hostingController, animated: true) } - present(navigationWrapper, animated: true) deviceManager.analyticsServicesManager.didDisplayCarbEntryScreen() } diff --git a/Loop/View Models/BolusEntryViewModel.swift b/Loop/View Models/BolusEntryViewModel.swift index f572e35dfd..a86f20e0cc 100644 --- a/Loop/View Models/BolusEntryViewModel.swift +++ b/Loop/View Models/BolusEntryViewModel.swift @@ -162,7 +162,7 @@ final class BolusEntryViewModel: ObservableObject { // MARK: - Initialization init( - delegate: BolusEntryViewModelDelegate, + delegate: BolusEntryViewModelDelegate?, now: @escaping () -> Date = { Date() }, screenWidth: CGFloat, debounceIntervalMilliseconds: Int = 400, diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift new file mode 100644 index 0000000000..b8b617a806 --- /dev/null +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -0,0 +1,194 @@ +// +// CarbEntryViewModel.swift +// Loop +// +// Created by Noah Brauner on 7/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import HealthKit +import Combine + +protocol CarbEntryViewModelDelegate: AnyObject, BolusEntryViewModelDelegate { + var analyticsServicesManager: AnalyticsServicesManager { get } + var defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes { get } +} + +final class CarbEntryViewModel: ObservableObject { + enum Alert: Identifiable { + var id: Self { + return self + } + + case maxQuantityExceded + case warningQuantityValidation + } + + @Published var alert: CarbEntryViewModel.Alert? + + @Published var bolusViewModel: BolusEntryViewModel? + + let shouldBeginEditingQuantity: Bool + + @Published var carbsQuantity: Double? = nil + var preferredCarbUnit = HKUnit.gram() + var maxCarbEntryQuantity = LoopConstants.maxCarbEntryQuantity + var warningCarbEntryQuantity = LoopConstants.warningCarbEntryQuantity + + @Published var time = Date() + private var date = Date() + var minimumDate: Date { + get { date.addingTimeInterval(.hours(-12)) } + } + var maximumDate: Date { + get { date.addingTimeInterval(.hours(1)) } + } + + @Published var foodType = "" + @Published var selectedDefaultAbsorptionTimeEmoji: String = "" + @Published var usesCustomFoodType = false + @Published var absorptionTimeWasEdited = false // if true, selecting an emoji will not alter the absorption time + + @Published var absorptionTime: TimeInterval + let defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes + let minAbsorptionTime = LoopConstants.minCarbAbsorptionTime + let maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime + var absorptionRimesRange: ClosedRange { + return minAbsorptionTime...maxAbsorptionTime + } + + weak var delegate: CarbEntryViewModelDelegate? + + private lazy var cancellables = Set() + + /// Initalizer for when`CarbEntryView` is presented from the home screen + init(delegate: CarbEntryViewModelDelegate) { + self.delegate = delegate + self.absorptionTime = delegate.defaultAbsorptionTimes.medium + self.defaultAbsorptionTimes = delegate.defaultAbsorptionTimes + self.shouldBeginEditingQuantity = true + + observeAbsorptionTimeChange() + } + + /// Initalizer for when`CarbEntryView` has an entry to edit + init(delegate: CarbEntryViewModelDelegate, originalCarbEntry: StoredCarbEntry) { + self.delegate = delegate + self.originalCarbEntry = originalCarbEntry + self.defaultAbsorptionTimes = delegate.defaultAbsorptionTimes + + self.carbsQuantity = originalCarbEntry.quantity.doubleValue(for: preferredCarbUnit) + self.time = originalCarbEntry.startDate + self.foodType = originalCarbEntry.foodType ?? "" + self.absorptionTime = originalCarbEntry.absorptionTime ?? .hours(3) + self.absorptionTimeWasEdited = true + self.usesCustomFoodType = true + self.shouldBeginEditingQuantity = false + } + + var originalCarbEntry: StoredCarbEntry? = nil + + private var updatedCarbEntry: NewCarbEntry? { + if let quantity = carbsQuantity, quantity != 0 { + if let o = originalCarbEntry, o.quantity.doubleValue(for: preferredCarbUnit) == quantity && o.startDate == time && o.foodType == foodType && o.absorptionTime == absorptionTime { + return nil // No changes were made + } + + return NewCarbEntry( + date: date, + quantity: HKQuantity(unit: preferredCarbUnit, doubleValue: quantity), + startDate: time, + foodType: usesCustomFoodType ? foodType : selectedDefaultAbsorptionTimeEmoji, + absorptionTime: absorptionTime + ) + } + else { + return nil + } + } + + var saveFavoriteFoodButtonDisabled: Bool { + get { + if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, foodType != "" { + return false + } + return true + } + } + + var continueButtonDisabled: Bool { + get { updatedCarbEntry == nil } + } + + // MARK: - Continue to Bolus and Carb Quantity Warnings + func continueToBolus() { + guard updatedCarbEntry != nil else { + return + } + + validateInputAndContinue() + } + + private func validateInputAndContinue() { + guard absorptionTime <= maxAbsorptionTime else { + return + } + + guard let carbsQuantity, carbsQuantity > 0 else { return } + let quantity = HKQuantity(unit: preferredCarbUnit, doubleValue: carbsQuantity) + if quantity.compare(maxCarbEntryQuantity) == .orderedDescending { + self.alert = .maxQuantityExceded + return + } + else if quantity.compare(warningCarbEntryQuantity) == .orderedDescending { + self.alert = .warningQuantityValidation + return + } + + Task { @MainActor in + setBolusViewModel() + } + } + + @MainActor private func setBolusViewModel() { + let viewModel = BolusEntryViewModel( + delegate: delegate, + screenWidth: UIScreen.main.bounds.width, + originalCarbEntry: originalCarbEntry, + potentialCarbEntry: updatedCarbEntry, + selectedCarbAbsorptionTimeEmoji: selectedDefaultAbsorptionTimeEmoji + ) + Task { + await viewModel.generateRecommendationAndStartObserving() + } + + viewModel.analyticsServicesManager = delegate?.analyticsServicesManager + bolusViewModel = viewModel + + delegate?.analyticsServicesManager.didDisplayBolusScreen() + } + + func clearAlert() { + self.alert = nil + } + + func clearAlertAndContinueToBolus() { + self.alert = nil + Task { @MainActor in + setBolusViewModel() + } + } + + // MARK: - Utility + private func observeAbsorptionTimeChange() { + $absorptionTime + .receive(on: RunLoop.main) + .dropFirst() + .sink { [weak self] _ in + self?.absorptionTimeWasEdited = true + } + .store(in: &cancellables) + } +} diff --git a/Loop/Views/CarbEntryView.swift b/Loop/Views/CarbEntryView.swift new file mode 100644 index 0000000000..dc88e7dd6d --- /dev/null +++ b/Loop/Views/CarbEntryView.swift @@ -0,0 +1,180 @@ +// +// CarbEntryView.swift +// Loop +// +// Created by Noah Brauner on 7/19/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import LoopKitUI +import HealthKit + +struct CarbEntryView: View, HorizontalSizeClassOverride { + @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference + @Environment(\.dismissAction) private var dismiss + + @ObservedObject var viewModel: CarbEntryViewModel + + @State private var expandedRow: Row? + + @State private var showHowAbsorptionTimeWorks = false + + private let isNewEntry: Bool + + init(viewModel: CarbEntryViewModel) { + if viewModel.shouldBeginEditingQuantity { + expandedRow = .amountConsumed + } + isNewEntry = viewModel.originalCarbEntry == nil + self.viewModel = viewModel + } + + var body: some View { + if isNewEntry { + NavigationView { + let title = NSLocalizedString("carb-entry-title-add", value: "Add Carb Entry", comment: "The title of the view controller to create a new carb entry") + content + .navigationBarTitle(title, displayMode: .inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + dismissButton + } + + ToolbarItem(placement: .navigationBarTrailing) { + continueButton + } + } + + } + } + else { + content + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + continueButton + } + } + } + } + + private var content: some View { + ZStack { + Color(.systemGroupedBackground) + .edgesIgnoringSafeArea(.all) + + ScrollView { + mainCard + .padding(.top, 8) + + continueActionButton + + let isBolusViewActive = Binding(get: { viewModel.bolusViewModel != nil }, set: { _, _ in viewModel.bolusViewModel = nil }) + NavigationLink(destination: bolusView, isActive: isBolusViewActive) { + EmptyView() + } + .frame(width: 0, height: 0) + .opacity(0) + .accessibility(hidden: true) + } + } + .alert(item: $viewModel.alert, content: alert(for:)) + .sheet(isPresented: $showHowAbsorptionTimeWorks) { + HowAbsorptionTimeWorksView() + } + } + + private var mainCard: some View { + VStack(spacing: 10) { + CarbQuantityRow(quantity: $viewModel.carbsQuantity, title: NSLocalizedString("Amount Consumed", comment: "Label for carb quantity entry row on carb entry screen"), preferredCarbUnit: viewModel.preferredCarbUnit, expandedRow: $expandedRow, row: Row.amountConsumed) + + CardSectionDivider() + + DatePickerRow(date: $viewModel.time, minimumDate: viewModel.minimumDate, maximumDate: viewModel.maximumDate, expandedRow: $expandedRow, row: Row.time) + + CardSectionDivider() + + FoodTypeRow(foodType: $viewModel.foodType, absorptionTime: $viewModel.absorptionTime, selectedDefaultAbsorptionTimeEmoji: $viewModel.selectedDefaultAbsorptionTimeEmoji, usesCustomFoodType: $viewModel.usesCustomFoodType, absorptionTimeWasEdited: $viewModel.absorptionTimeWasEdited, defaultAbsorptionTimes: viewModel.defaultAbsorptionTimes, expandedRow: $expandedRow, row: .foodType) + + CardSectionDivider() + + AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, validDurationRange: viewModel.absorptionRimesRange, expandedRow: $expandedRow, row: Row.absorptionTime, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) + .padding(.bottom, 2) + } + .padding(.vertical, 12) + .padding(.horizontal) + .background(CardBackground()) + .padding(.horizontal) + } + + @ViewBuilder + private var bolusView: some View { + if let viewModel = viewModel.bolusViewModel { + BolusEntryView(viewModel: viewModel) + .environmentObject(displayGlucosePreference) + .environment(\.dismissAction, dismiss) + } + } + + private func clearExpandedRow() { + self.expandedRow = nil + } + + private func alert(for alert: CarbEntryViewModel.Alert) -> SwiftUI.Alert { + switch alert { + case .maxQuantityExceded: + let message = String( + format: NSLocalizedString("The maximum allowed amount is %@ grams.", comment: "Alert body displayed for quantity greater than max (1: maximum quantity in grams)"), + NumberFormatter.localizedString(from: NSNumber(value: viewModel.maxCarbEntryQuantity.doubleValue(for: viewModel.preferredCarbUnit)), number: .none) + ) + let okMessage = NSLocalizedString("com.loudnate.LoopKit.errorAlertActionTitle", value: "OK", comment: "The title of the action used to dismiss an error alert") + return SwiftUI.Alert( + title: Text("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered"), + message: Text(message), + dismissButton: .cancel(Text(okMessage), action: viewModel.clearAlert) + ) + case .warningQuantityValidation: + let message = String( + format: NSLocalizedString("Did you intend to enter %1$@ grams as the amount of carbohydrates for this meal?", comment: "Alert body when entered carbohydrates is greater than threshold (1: entered quantity in grams)"), + NumberFormatter.localizedString(from: NSNumber(value: viewModel.carbsQuantity ?? 0), number: .none) + ) + return SwiftUI.Alert( + title: Text("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered"), + message: Text(message), + primaryButton: .default(Text("No, edit amount", comment: "The title of the action used when rejecting the the amount of carbohydrates entered."), action: viewModel.clearAlert), + secondaryButton: .cancel(Text("Yes", comment: "The title of the action used when confirming entered amount of carbohydrates."), action: viewModel.clearAlertAndContinueToBolus) + ) + } + } +} + +extension CarbEntryView { + private var dismissButton: some View { + Button(action: dismiss) { + Text("Cancel") + } + } + + private var continueButton: some View { + Button(action: viewModel.continueToBolus) { + Text("Continue") + } + .disabled(viewModel.continueButtonDisabled) + } + + private var continueActionButton: some View { + Button(action: viewModel.continueToBolus) { + Text("Continue") + } + .buttonStyle(ActionButtonStyle()) + .padding() + .disabled(viewModel.continueButtonDisabled) + } +} + +extension CarbEntryView { + enum Row { + case amountConsumed, time, foodType, absorptionTime + } +} From 4a8ae677ec1dedb4926acf0013b7c2b8ea0dae9a Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Thu, 10 Aug 2023 13:10:20 -0400 Subject: [PATCH 13/22] [LOOP-4687] favorite food carb entry flow --- Loop/View Models/CarbEntryViewModel.swift | 64 ++++++++++++++++- Loop/Views/CarbEntryView.swift | 86 ++++++++++++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index b8b617a806..3a27557b32 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -50,6 +50,7 @@ final class CarbEntryViewModel: ObservableObject { @Published var selectedDefaultAbsorptionTimeEmoji: String = "" @Published var usesCustomFoodType = false @Published var absorptionTimeWasEdited = false // if true, selecting an emoji will not alter the absorption time + private var absorptionEditIsProgrammatic = false // needed for when absorption time is changed due to favorite food selection, so that absorptionTimeWasEdited does not get set to true @Published var absorptionTime: TimeInterval let defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes @@ -59,6 +60,9 @@ final class CarbEntryViewModel: ObservableObject { return minAbsorptionTime...maxAbsorptionTime } + @Published var favoriteFoods = UserDefaults.standard.favoriteFoods + @Published var selectedFavoriteFoodIndex = -1 + weak var delegate: CarbEntryViewModelDelegate? private lazy var cancellables = Set() @@ -71,6 +75,8 @@ final class CarbEntryViewModel: ObservableObject { self.shouldBeginEditingQuantity = true observeAbsorptionTimeChange() + observeFavoriteFoodChange() + observeFavoriteFoodIndexChange() } /// Initalizer for when`CarbEntryView` has an entry to edit @@ -89,6 +95,7 @@ final class CarbEntryViewModel: ObservableObject { } var originalCarbEntry: StoredCarbEntry? = nil + private var favoriteFood: FavoriteFood? = nil private var updatedCarbEntry: NewCarbEntry? { if let quantity = carbsQuantity, quantity != 0 { @@ -111,7 +118,7 @@ final class CarbEntryViewModel: ObservableObject { var saveFavoriteFoodButtonDisabled: Bool { get { - if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, foodType != "" { + if let carbsQuantity, 0...maxCarbEntryQuantity.doubleValue(for: preferredCarbUnit) ~= carbsQuantity, foodType != "", selectedFavoriteFoodIndex == -1 { return false } return true @@ -142,7 +149,7 @@ final class CarbEntryViewModel: ObservableObject { self.alert = .maxQuantityExceded return } - else if quantity.compare(warningCarbEntryQuantity) == .orderedDescending { + else if quantity.compare(warningCarbEntryQuantity) == .orderedDescending, selectedFavoriteFoodIndex == -1 { self.alert = .warningQuantityValidation return } @@ -181,13 +188,64 @@ final class CarbEntryViewModel: ObservableObject { } } + // MARK: - Favorite Foods + func onFavoriteFoodSave(_ food: NewFavoriteFood) { + let newStoredFood = StoredFavoriteFood(name: food.name, carbsQuantity: food.carbsQuantity, foodType: food.foodType, absorptionTime: food.absorptionTime) + favoriteFoods.append(newStoredFood) + selectedFavoriteFoodIndex = favoriteFoods.count - 1 + } + + private func observeFavoriteFoodIndexChange() { + $selectedFavoriteFoodIndex + .receive(on: RunLoop.main) + .dropFirst() + .sink { [weak self] index in + self?.favoriteFoodSelected(at: index) + } + .store(in: &cancellables) + } + + private func observeFavoriteFoodChange() { + $favoriteFoods + .dropFirst() + .removeDuplicates() + .sink { newValue in + UserDefaults.standard.favoriteFoods = newValue + } + .store(in: &cancellables) + } + + private func favoriteFoodSelected(at index: Int) { + self.absorptionEditIsProgrammatic = true + if index == -1 { + self.carbsQuantity = 0 + self.foodType = "" + self.absorptionTime = defaultAbsorptionTimes.medium + self.absorptionTimeWasEdited = false + self.usesCustomFoodType = false + } + else { + let food = favoriteFoods[index] + self.carbsQuantity = food.carbsQuantity.doubleValue(for: preferredCarbUnit) + self.foodType = food.foodType + self.absorptionTime = food.absorptionTime + self.absorptionTimeWasEdited = true + self.usesCustomFoodType = true + } + } + // MARK: - Utility private func observeAbsorptionTimeChange() { $absorptionTime .receive(on: RunLoop.main) .dropFirst() .sink { [weak self] _ in - self?.absorptionTimeWasEdited = true + if self?.absorptionEditIsProgrammatic == true { + self?.absorptionEditIsProgrammatic = false + } + else { + self?.absorptionTimeWasEdited = true + } } .store(in: &cancellables) } diff --git a/Loop/Views/CarbEntryView.swift b/Loop/Views/CarbEntryView.swift index dc88e7dd6d..85ef37077d 100644 --- a/Loop/Views/CarbEntryView.swift +++ b/Loop/Views/CarbEntryView.swift @@ -20,6 +20,7 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { @State private var expandedRow: Row? @State private var showHowAbsorptionTimeWorks = false + @State private var showAddFavoriteFood = false private let isNewEntry: Bool @@ -70,6 +71,10 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { continueActionButton + if isNewEntry { + favoriteFoodsCard + } + let isBolusViewActive = Binding(get: { viewModel.bolusViewModel != nil }, set: { _, _ in viewModel.bolusViewModel = nil }) NavigationLink(destination: bolusView, isActive: isBolusViewActive) { EmptyView() @@ -80,6 +85,9 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { } } .alert(item: $viewModel.alert, content: alert(for:)) + .sheet(isPresented: $showAddFavoriteFood, onDismiss: clearExpandedRow) { + AddEditFavoriteFoodView(carbsQuantity: $viewModel.carbsQuantity.wrappedValue, foodType: $viewModel.foodType.wrappedValue, absorptionTime: $viewModel.absorptionTime.wrappedValue, onSave: onFavoriteFoodSave(_:)) + } .sheet(isPresented: $showHowAbsorptionTimeWorks) { HowAbsorptionTimeWorksView() } @@ -171,10 +179,86 @@ extension CarbEntryView { .padding() .disabled(viewModel.continueButtonDisabled) } + + private var favoriteFoodsCard: some View { + VStack(alignment: .leading, spacing: 6) { + Text("FAVORITE FOODS") + .font(.footnote) + .foregroundColor(.secondary) + .padding(.horizontal, 26) + + VStack(spacing: 10) { + if !viewModel.favoriteFoods.isEmpty { + VStack { + HStack { + Text("Choose Favorite:") + + let selectedFavorite = favoritedFoodTextFromIndex(viewModel.selectedFavoriteFoodIndex) + Text(selectedFavorite) + .minimumScaleFactor(0.8) + .frame(maxWidth: .infinity, alignment: .trailing) + } + + if expandedRow == .favoriteFoodSelection { + Picker("", selection: $viewModel.selectedFavoriteFoodIndex) { + ForEach(-1.. String { + if index == -1 { + return "None" + } + else { + let food = viewModel.favoriteFoods[index] + return "\(food.name) \(food.foodType)" + } + } + + private func saveAsFavoriteFood() { + self.showAddFavoriteFood = true + } + + private func onFavoriteFoodSave(_ food: NewFavoriteFood) { + clearExpandedRow() + self.showAddFavoriteFood = false + viewModel.onFavoriteFoodSave(food) + } } extension CarbEntryView { enum Row { - case amountConsumed, time, foodType, absorptionTime + case amountConsumed, time, foodType, absorptionTime, favoriteFoodSelection } } From 5baebff68c1ad4a490cd2f0724d6cb3029f5134e Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Thu, 10 Aug 2023 13:24:49 -0400 Subject: [PATCH 14/22] [LOOP-4685] favorite foods feature flag; UserDefaults --- Common/FeatureFlags.swift | 17 ++++++++++++++++- Loop/Views/CarbEntryView.swift | 2 +- Loop/Views/SettingsView.swift | 4 +++- LoopCore/NSUserDefaults.swift | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Common/FeatureFlags.swift b/Common/FeatureFlags.swift index fff5f98f07..0c6440b564 100644 --- a/Common/FeatureFlags.swift +++ b/Common/FeatureFlags.swift @@ -266,7 +266,8 @@ extension FeatureFlagConfiguration : CustomDebugStringConvertible { "* adultChildInsulinModelSelectionEnabled: \(adultChildInsulinModelSelectionEnabled)", "* profileExpirationSettingsViewEnabled: \(profileExpirationSettingsViewEnabled)", "* missedMealNotifications: \(missedMealNotifications)", - "* allowAlgorithmExperiments: \(allowAlgorithmExperiments)" + "* allowAlgorithmExperiments: \(allowAlgorithmExperiments)", + "* allowExperimentalFeatures: \(allowExperimentalFeatures)" ].joined(separator: "\n") } } @@ -290,6 +291,20 @@ extension FeatureFlagConfiguration { #endif } + var allowExperimentalFeatures: Bool { + #if EXPERIMENTAL_FEATURES_ENABLED + return true + #elseif EXPERIMENTAL_FEATURES_ENABLED_CONDITIONALLY + if debugEnabled { + return true + } else { + return UserDefaults.appGroup?.allowExperimentalFeatures ?? false + } + #else + return false + #endif + } + var allowSimulators: Bool { #if SIMULATORS_ENABLED return true diff --git a/Loop/Views/CarbEntryView.swift b/Loop/Views/CarbEntryView.swift index 85ef37077d..fa32666c4b 100644 --- a/Loop/Views/CarbEntryView.swift +++ b/Loop/Views/CarbEntryView.swift @@ -71,7 +71,7 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { continueActionButton - if isNewEntry { + if isNewEntry, FeatureFlags.allowExperimentalFeatures { favoriteFoodsCard } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 942d4675b8..0b4cb55133 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -56,7 +56,9 @@ public struct SettingsView: View { configurationSection } deviceSettingsSection - favoriteFoodsSection + if FeatureFlags.allowExperimentalFeatures { + favoriteFoodsSection + } if (viewModel.pumpManagerSettingsViewModel.isTestingDevice || viewModel.cgmManagerSettingsViewModel.isTestingDevice) && viewModel.showDeleteTestData { deleteDataSection } diff --git a/LoopCore/NSUserDefaults.swift b/LoopCore/NSUserDefaults.swift index 40e45612a2..93fa7e17d6 100644 --- a/LoopCore/NSUserDefaults.swift +++ b/LoopCore/NSUserDefaults.swift @@ -19,6 +19,7 @@ extension UserDefaults { case bedtime = "com.loopkit.Loop.bedtime" case lastProfileExpirationAlertDate = "com.loopkit.Loop.lastProfileExpirationAlertDate" case allowDebugFeatures = "com.loopkit.Loop.allowDebugFeatures" + case allowExperimentalFeatures = "com.loopkit.Loop.allowExperimentalFeatures" case allowSimulators = "com.loopkit.Loop.allowSimulators" case LastMissedMealNotification = "com.loopkit.Loop.lastMissedMealNotification" case userRequestedLoopReset = "com.loopkit.Loop.userRequestedLoopReset" @@ -148,6 +149,10 @@ extension UserDefaults { } } + public var allowExperimentalFeatures: Bool { + return bool(forKey: Key.allowExperimentalFeatures.rawValue) + } + public var allowSimulators: Bool { return bool(forKey: Key.allowSimulators.rawValue) } From c50eabd6929da13217f45c15ca73275064eb7da8 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Fri, 11 Aug 2023 10:13:35 -0400 Subject: [PATCH 15/22] Formatter/localization updates --- Loop/View Models/FavoriteFoodsViewModel.swift | 10 ++++++++++ Loop/Views/FavoriteFoodsView.swift | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Loop/View Models/FavoriteFoodsViewModel.swift b/Loop/View Models/FavoriteFoodsViewModel.swift index 3138e097dc..48934d1c10 100644 --- a/Loop/View Models/FavoriteFoodsViewModel.swift +++ b/Loop/View Models/FavoriteFoodsViewModel.swift @@ -7,6 +7,7 @@ // import SwiftUI +import HealthKit import LoopKit import Combine @@ -18,6 +19,15 @@ final class FavoriteFoodsViewModel: ObservableObject { @Published var isEditViewActive = false @Published var isAddViewActive = false + var preferredCarbUnit = HKUnit.gram() + lazy var carbFormatter = QuantityFormatter(for: preferredCarbUnit) + lazy var absorptionTimeFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute] + formatter.unitsStyle = .abbreviated + return formatter + }() + private lazy var cancellables = Set() init() { diff --git a/Loop/Views/FavoriteFoodsView.swift b/Loop/Views/FavoriteFoodsView.swift index 5108b17d3b..c2bb941c26 100644 --- a/Loop/Views/FavoriteFoodsView.swift +++ b/Loop/Views/FavoriteFoodsView.swift @@ -30,7 +30,7 @@ struct FavoriteFoodsView: View { else { Section(header: listHeader) { ForEach(viewModel.favoriteFoods) { food in - FavoriteFoodListRow(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: viewModel.onFoodDelete(_:)) + FavoriteFoodListRow(food: food, foodToConfirmDeleteId: $foodToConfirmDeleteId, onFoodTap: onFoodTap(_:), onFoodDelete: viewModel.onFoodDelete(_:), carbFormatter: viewModel.carbFormatter, absorptionTimeFormatter: viewModel.absorptionTimeFormatter, preferredCarbUnit: viewModel.preferredCarbUnit) .environment(\.editMode, self.$editMode) .listRowInsets(EdgeInsets()) } @@ -52,7 +52,7 @@ struct FavoriteFoodsView: View { EmptyView() } - NavigationLink(destination: FavoriteFoodDetailView(food: viewModel.selectedFood, onFoodDelete: viewModel.onFoodDelete(_:)), isActive: $viewModel.isDetailViewActive) { + NavigationLink(destination: FavoriteFoodDetailView(food: viewModel.selectedFood, onFoodDelete: viewModel.onFoodDelete(_:), carbFormatter: viewModel.carbFormatter, absorptionTimeFormatter: viewModel.absorptionTimeFormatter, preferredCarbUnit: viewModel.preferredCarbUnit), isActive: $viewModel.isDetailViewActive) { EmptyView() } } From e589d2fa15d100170a4531d870931d8d586aac29 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Fri, 11 Aug 2023 11:01:17 -0400 Subject: [PATCH 16/22] Simplify expanded row code --- Loop/Views/AddEditFavoriteFoodView.swift | 15 ++++++++++----- Loop/Views/CarbEntryView.swift | 13 +++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Loop/Views/AddEditFavoriteFoodView.swift b/Loop/Views/AddEditFavoriteFoodView.swift index 05ee94c551..308f2317fd 100644 --- a/Loop/Views/AddEditFavoriteFoodView.swift +++ b/Loop/Views/AddEditFavoriteFoodView.swift @@ -88,19 +88,24 @@ struct AddEditFavoriteFoodView: View { private var card: some View { VStack(spacing: 10) { - TextFieldRow(text: $viewModel.name, title: "Name", placeholder: "Apple", expandedRow: $expandedRow, row: .name) + var nameFocused: Binding = Binding(get: { expandedRow == .name }, set: { expandedRow = $0 ? .name : nil }) + var carbQuantityFocused: Binding = Binding(get: { expandedRow == .carbQuantity }, set: { expandedRow = $0 ? .carbQuantity : nil }) + var foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) + var absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) + + TextFieldRow(text: $viewModel.name, isFocused: nameFocused, title: "Name", placeholder: "Apple") CardSectionDivider() - CarbQuantityRow(quantity: $viewModel.carbsQuantity, title: "Carb Quantity", preferredCarbUnit: viewModel.preferredCarbUnit, expandedRow: $expandedRow, row: Row.amountConsumed) + CarbQuantityRow(quantity: $viewModel.carbsQuantity, isFocused: carbQuantityFocused, title: "Carb Quantity", preferredCarbUnit: viewModel.preferredCarbUnit) CardSectionDivider() - EmojiRow(emojiType: .food, text: $viewModel.foodType, title: "Food Type", expandedRow: $expandedRow, row: .foodType) + EmojiRow(text: $viewModel.foodType, isFocused: foodTypeFocused, emojiType: .food, title: "Food Type") CardSectionDivider() - AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, validDurationRange: viewModel.absorptionRimesRange, expandedRow: $expandedRow, row: Row.absorptionTime, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) + AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, isFocused: absorptionTimeFocused, validDurationRange: viewModel.absorptionRimesRange, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) .padding(.bottom, 2) } .padding(.vertical, 12) @@ -163,6 +168,6 @@ extension AddEditFavoriteFoodView { extension AddEditFavoriteFoodView { enum Row { - case name, amountConsumed, foodType, absorptionTime + case name, carbQuantity, foodType, absorptionTime } } diff --git a/Loop/Views/CarbEntryView.swift b/Loop/Views/CarbEntryView.swift index fa32666c4b..1add1b85b4 100644 --- a/Loop/Views/CarbEntryView.swift +++ b/Loop/Views/CarbEntryView.swift @@ -95,19 +95,24 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { private var mainCard: some View { VStack(spacing: 10) { - CarbQuantityRow(quantity: $viewModel.carbsQuantity, title: NSLocalizedString("Amount Consumed", comment: "Label for carb quantity entry row on carb entry screen"), preferredCarbUnit: viewModel.preferredCarbUnit, expandedRow: $expandedRow, row: Row.amountConsumed) + var amountConsumedFocused: Binding = Binding(get: { expandedRow == .amountConsumed }, set: { expandedRow = $0 ? .amountConsumed : nil }) + var timeFocused: Binding = Binding(get: { expandedRow == .time }, set: { expandedRow = $0 ? .time : nil }) + var foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) + var absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) + + CarbQuantityRow(quantity: $viewModel.carbsQuantity, isFocused: amountConsumedFocused, title: NSLocalizedString("Amount Consumed", comment: "Label for carb quantity entry row on carb entry screen"), preferredCarbUnit: viewModel.preferredCarbUnit) CardSectionDivider() - DatePickerRow(date: $viewModel.time, minimumDate: viewModel.minimumDate, maximumDate: viewModel.maximumDate, expandedRow: $expandedRow, row: Row.time) + DatePickerRow(date: $viewModel.time, isFocused: timeFocused, minimumDate: viewModel.minimumDate, maximumDate: viewModel.maximumDate) CardSectionDivider() - FoodTypeRow(foodType: $viewModel.foodType, absorptionTime: $viewModel.absorptionTime, selectedDefaultAbsorptionTimeEmoji: $viewModel.selectedDefaultAbsorptionTimeEmoji, usesCustomFoodType: $viewModel.usesCustomFoodType, absorptionTimeWasEdited: $viewModel.absorptionTimeWasEdited, defaultAbsorptionTimes: viewModel.defaultAbsorptionTimes, expandedRow: $expandedRow, row: .foodType) + FoodTypeRow(foodType: $viewModel.foodType, absorptionTime: $viewModel.absorptionTime, selectedDefaultAbsorptionTimeEmoji: $viewModel.selectedDefaultAbsorptionTimeEmoji, usesCustomFoodType: $viewModel.usesCustomFoodType, absorptionTimeWasEdited: $viewModel.absorptionTimeWasEdited, isFocused: foodTypeFocused, defaultAbsorptionTimes: viewModel.defaultAbsorptionTimes) CardSectionDivider() - AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, validDurationRange: viewModel.absorptionRimesRange, expandedRow: $expandedRow, row: Row.absorptionTime, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) + AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, isFocused: absorptionTimeFocused, validDurationRange: viewModel.absorptionRimesRange, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks) .padding(.bottom, 2) } .padding(.vertical, 12) From e5fd6b4f6a54de91f2eea826976aa3b9c17790a8 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Fri, 11 Aug 2023 17:34:18 -0400 Subject: [PATCH 17/22] CarbEntryView DIY features: missed meal, override warning --- .../StatusTableViewController.swift | 3 + Loop/View Models/CarbEntryViewModel.swift | 67 +++++++++++- Loop/Views/AddEditFavoriteFoodView.swift | 8 +- Loop/Views/CarbEntryView.swift | 102 +++++++++++++----- 4 files changed, 149 insertions(+), 31 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index b4da816dbd..cdac208d13 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1371,6 +1371,9 @@ final class StatusTableViewController: LoopChartsTableViewController { present(navigationWrapper, animated: true) } else { let viewModel = CarbEntryViewModel(delegate: deviceManager) + if let activity { + viewModel.restoreUserActivityState(activity) + } let carbEntryView = CarbEntryView(viewModel: viewModel) .environmentObject(deviceManager.displayGlucosePreference) let hostingController = DismissibleHostingController(rootView: carbEntryView, isModalInPresentation: false) diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index 3a27557b32..d04ddba78e 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -26,8 +26,27 @@ final class CarbEntryViewModel: ObservableObject { case warningQuantityValidation } - @Published var alert: CarbEntryViewModel.Alert? + enum Warning: Identifiable { + var id: Self { + return self + } + + var priority: Int { + switch self { + case .entryIsMissedMeal: + return 1 + case .overrideInProgress: + return 2 + } + } + + case entryIsMissedMeal + case overrideInProgress + } + @Published var alert: CarbEntryViewModel.Alert? + @Published var warnings: Set = [] + @Published var bolusViewModel: BolusEntryViewModel? let shouldBeginEditingQuantity: Bool @@ -77,6 +96,7 @@ final class CarbEntryViewModel: ObservableObject { observeAbsorptionTimeChange() observeFavoriteFoodChange() observeFavoriteFoodIndexChange() + observeLoopUpdates() } /// Initalizer for when`CarbEntryView` has an entry to edit @@ -92,6 +112,8 @@ final class CarbEntryViewModel: ObservableObject { self.absorptionTimeWasEdited = true self.usesCustomFoodType = true self.shouldBeginEditingQuantity = false + + observeLoopUpdates() } var originalCarbEntry: StoredCarbEntry? = nil @@ -235,6 +257,49 @@ final class CarbEntryViewModel: ObservableObject { } // MARK: - Utility + func restoreUserActivityState(_ activity: NSUserActivity) { + if let entry = activity.newCarbEntry { + time = entry.date + carbsQuantity = entry.quantity.doubleValue(for: preferredCarbUnit) + + if let foodType = entry.foodType { + self.foodType = foodType + usesCustomFoodType = true + } + + if let absorptionTime = entry.absorptionTime { + self.absorptionTime = absorptionTime + absorptionTimeWasEdited = true + } + + if activity.entryisMissedMeal { + warnings.insert(.entryIsMissedMeal) + } + } + } + + private func observeLoopUpdates() { + self.checkIfOverrideEnabled() + NotificationCenter.default + .publisher(for: .LoopDataUpdated) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.checkIfOverrideEnabled() + } + .store(in: &cancellables) + } + + private func checkIfOverrideEnabled() { + if let managerSettings = delegate?.settings { + if let overrideSettings = managerSettings.scheduleOverride?.settings, overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 { + self.warnings.insert(.overrideInProgress) + } + else { + self.warnings.remove(.overrideInProgress) + } + } + } + private func observeAbsorptionTimeChange() { $absorptionTime .receive(on: RunLoop.main) diff --git a/Loop/Views/AddEditFavoriteFoodView.swift b/Loop/Views/AddEditFavoriteFoodView.swift index 308f2317fd..b647523a13 100644 --- a/Loop/Views/AddEditFavoriteFoodView.swift +++ b/Loop/Views/AddEditFavoriteFoodView.swift @@ -88,10 +88,10 @@ struct AddEditFavoriteFoodView: View { private var card: some View { VStack(spacing: 10) { - var nameFocused: Binding = Binding(get: { expandedRow == .name }, set: { expandedRow = $0 ? .name : nil }) - var carbQuantityFocused: Binding = Binding(get: { expandedRow == .carbQuantity }, set: { expandedRow = $0 ? .carbQuantity : nil }) - var foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) - var absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) + let nameFocused: Binding = Binding(get: { expandedRow == .name }, set: { expandedRow = $0 ? .name : nil }) + let carbQuantityFocused: Binding = Binding(get: { expandedRow == .carbQuantity }, set: { expandedRow = $0 ? .carbQuantity : nil }) + let foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) + let absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) TextFieldRow(text: $viewModel.name, isFocused: nameFocused, title: "Name", placeholder: "Apple") diff --git a/Loop/Views/CarbEntryView.swift b/Loop/Views/CarbEntryView.swift index 1add1b85b4..14c6b2c460 100644 --- a/Loop/Views/CarbEntryView.swift +++ b/Loop/Views/CarbEntryView.swift @@ -66,6 +66,8 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { .edgesIgnoringSafeArea(.all) ScrollView { + warningsCard + mainCard .padding(.top, 8) @@ -95,10 +97,10 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { private var mainCard: some View { VStack(spacing: 10) { - var amountConsumedFocused: Binding = Binding(get: { expandedRow == .amountConsumed }, set: { expandedRow = $0 ? .amountConsumed : nil }) - var timeFocused: Binding = Binding(get: { expandedRow == .time }, set: { expandedRow = $0 ? .time : nil }) - var foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) - var absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) + let amountConsumedFocused: Binding = Binding(get: { expandedRow == .amountConsumed }, set: { expandedRow = $0 ? .amountConsumed : nil }) + let timeFocused: Binding = Binding(get: { expandedRow == .time }, set: { expandedRow = $0 ? .time : nil }) + let foodTypeFocused: Binding = Binding(get: { expandedRow == .foodType }, set: { expandedRow = $0 ? .foodType : nil }) + let absorptionTimeFocused: Binding = Binding(get: { expandedRow == .absorptionTime }, set: { expandedRow = $0 ? .absorptionTime : nil }) CarbQuantityRow(quantity: $viewModel.carbsQuantity, isFocused: amountConsumedFocused, title: NSLocalizedString("Amount Consumed", comment: "Label for carb quantity entry row on carb entry screen"), preferredCarbUnit: viewModel.preferredCarbUnit) @@ -133,6 +135,49 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { private func clearExpandedRow() { self.expandedRow = nil } +} + +// MARK: - Warnings & Alerts +extension CarbEntryView { + private var warningsCard: some View { + ForEach(Array(viewModel.warnings).sorted(by: { $0.priority < $1.priority })) { warning in + warningView(for: warning) + .padding(.vertical, 8) + .padding(.horizontal) + .background(CardBackground()) + .padding(.horizontal) + .padding(.top, 8) + } + } + + private func warningView(for warning: CarbEntryViewModel.Warning) -> some View { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(triangleColor(for: warning)) + + Text(warningText(for: warning)) + .font(.caption) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func triangleColor(for warning: CarbEntryViewModel.Warning) -> Color { + switch warning { + case .entryIsMissedMeal: + return .critical + case .overrideInProgress: + return .warning + } + } + + private func warningText(for warning: CarbEntryViewModel.Warning) -> String { + switch warning { + case .entryIsMissedMeal: + return NSLocalizedString("Loop has detected an missed meal and estimated its size. Edit the carb amount to match the amount of any carbs you may have eaten.", comment: "Warning displayed when user is adding a meal from an missed meal notification") + case .overrideInProgress: + return NSLocalizedString("An active override is modifying your carb ratio and insulin sensitivity. If you don't want this to affect your bolus calculation and projected glucose, consider turning off the override.", comment: "Warning to ensure the carb entry is accurate during an override") + } + } private func alert(for alert: CarbEntryViewModel.Alert) -> SwiftUI.Alert { switch alert { @@ -162,29 +207,8 @@ struct CarbEntryView: View, HorizontalSizeClassOverride { } } +// MARK: - Favorite Foods Card extension CarbEntryView { - private var dismissButton: some View { - Button(action: dismiss) { - Text("Cancel") - } - } - - private var continueButton: some View { - Button(action: viewModel.continueToBolus) { - Text("Continue") - } - .disabled(viewModel.continueButtonDisabled) - } - - private var continueActionButton: some View { - Button(action: viewModel.continueToBolus) { - Text("Continue") - } - .buttonStyle(ActionButtonStyle()) - .padding() - .disabled(viewModel.continueButtonDisabled) - } - private var favoriteFoodsCard: some View { VStack(alignment: .leading, spacing: 6) { Text("FAVORITE FOODS") @@ -262,6 +286,32 @@ extension CarbEntryView { } } +// MARK: - Other UI Elements +extension CarbEntryView { + private var dismissButton: some View { + Button(action: dismiss) { + Text("Cancel") + } + } + + private var continueButton: some View { + Button(action: viewModel.continueToBolus) { + Text("Continue") + } + .disabled(viewModel.continueButtonDisabled) + } + + private var continueActionButton: some View { + Button(action: viewModel.continueToBolus) { + Text("Continue") + } + .buttonStyle(ActionButtonStyle()) + .padding() + .disabled(viewModel.continueButtonDisabled) + } + +} + extension CarbEntryView { enum Row { case amountConsumed, time, foodType, absorptionTime, favoriteFoodSelection From 8059823ef040d29d0b25a52c6ed1326e0365e9f1 Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Mon, 14 Aug 2023 09:58:04 -0400 Subject: [PATCH 18/22] Remove unused code, move files from LoopKit --- Loop.xcodeproj/project.pbxproj | 8 +- .../CarbEntryViewController.swift | 786 ------------------ .../RootNavigationController.swift | 18 - Loop/Views/FavoriteFoodDetailView.swift | 73 ++ 4 files changed, 77 insertions(+), 808 deletions(-) delete mode 100644 Loop/View Controllers/CarbEntryViewController.swift create mode 100644 Loop/Views/FavoriteFoodDetailView.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index a24c173993..abbaf19cfb 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */; }; 149A28BD2A853E6C00052EDF /* CarbEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */; }; + 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; 14B1736528AED9EE006CCD7C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14B1736428AED9EE006CCD7C /* Assets.xcassets */; }; @@ -253,7 +254,6 @@ 891B508524342BE1005DA578 /* CarbAndBolusFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */; }; 892A5D59222F0A27008961AB /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D58222F0A27008961AB /* Debug.swift */; }; 892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D682230C41D008961AB /* RangeReplaceableCollection.swift */; }; - 892D7C5123B54A15008A9656 /* CarbEntryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892D7C5023B54A14008A9656 /* CarbEntryViewController.swift */; }; 892FB4CD22040104005293EC /* OverridePresetRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FB4CC22040104005293EC /* OverridePresetRow.swift */; }; 892FB4CF220402C0005293EC /* OverrideSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FB4CE220402C0005293EC /* OverrideSelectionController.swift */; }; 894F6DD3243BCBDB00CCE676 /* Environment+SizeClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894F6DD2243BCBDB00CCE676 /* Environment+SizeClass.swift */; }; @@ -745,6 +745,7 @@ 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowAbsorptionTimeWorksView.swift; sourceTree = ""; }; 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; + 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 14B1736428AED9EE006CCD7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1178,7 +1179,6 @@ 892A5D58222F0A27008961AB /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = ""; }; 892A5D5A222F0D7C008961AB /* LoopTestingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = LoopTestingKit.framework; path = Carthage/Build/iOS/LoopTestingKit.framework; sourceTree = SOURCE_ROOT; }; 892A5D682230C41D008961AB /* RangeReplaceableCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RangeReplaceableCollection.swift; sourceTree = ""; }; - 892D7C5023B54A14008A9656 /* CarbEntryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewController.swift; sourceTree = ""; }; 892FB4CC22040104005293EC /* OverridePresetRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetRow.swift; sourceTree = ""; }; 892FB4CE220402C0005293EC /* OverrideSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideSelectionController.swift; sourceTree = ""; }; 894F6DD2243BCBDB00CCE676 /* Environment+SizeClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+SizeClass.swift"; sourceTree = ""; }; @@ -2208,7 +2208,6 @@ isa = PBXGroup; children = ( 43A51E1E1EB6D62A000736CC /* CarbAbsorptionViewController.swift */, - 892D7C5023B54A14008A9656 /* CarbEntryViewController.swift */, 43A51E201EB6DBDD000736CC /* LoopChartsTableViewController.swift */, 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */, C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */, @@ -2235,6 +2234,7 @@ 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */, A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */, C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */, + 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */, 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */, 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */, 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */, @@ -3687,7 +3687,6 @@ 1D49795824E7289700948F05 /* ServicesViewModel.swift in Sources */, 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */, DDC389FA2A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift in Sources */, - 892D7C5123B54A15008A9656 /* CarbEntryViewController.swift in Sources */, B4E202302661063E009421B5 /* AutomaticDosingStatus.swift in Sources */, C191D2A125B3ACAA00C26C0B /* DosingStrategySelectionView.swift in Sources */, A977A2F424ACFECF0059C207 /* CriticalEventLogExportManager.swift in Sources */, @@ -3699,6 +3698,7 @@ 1DB1065124467E18005542BD /* AlertManager.swift in Sources */, 1D9650C82523FBA100A1370B /* DeviceDataManager+BolusEntryViewModelDelegate.swift in Sources */, 43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */, + 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */, 1DDE274024AEA4F200796622 /* NotificationsCriticalAlertPermissionsView.swift in Sources */, A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */, 434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */, diff --git a/Loop/View Controllers/CarbEntryViewController.swift b/Loop/View Controllers/CarbEntryViewController.swift deleted file mode 100644 index 92b7ef7be4..0000000000 --- a/Loop/View Controllers/CarbEntryViewController.swift +++ /dev/null @@ -1,786 +0,0 @@ -// -// CarbEntryViewController.swift -// CarbKit -// -// Created by Nathan Racklyeft on 1/15/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import UIKit -import HealthKit -import LoopKit -import LoopKitUI -import LoopCore -import LoopUI - -final class CarbEntryViewController: LoopChartsTableViewController, IdentifiableClass { - - var defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes? { - didSet { - if let times = defaultAbsorptionTimes { - orderedAbsorptionTimes = [times.fast, times.medium, times.slow] - } - } - } - - fileprivate var orderedAbsorptionTimes = [TimeInterval]() - - var preferredCarbUnit = HKUnit.gram() - - private var glucoseUnit: HKUnit { - return deviceManager.displayGlucosePreference.unit - } - - var maxCarbEntryQuantity = LoopConstants.maxCarbEntryQuantity - - var warningCarbEntryQuantity = LoopConstants.warningCarbEntryQuantity - - /// Entry configuration values. Must be set before presenting. - var absorptionTimePickerInterval = TimeInterval(minutes: 30) - - var maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime - - var maximumDateFutureInterval = TimeInterval(hours: 4) - - var originalCarbEntry: StoredCarbEntry? { - didSet { - if let entry = originalCarbEntry { - quantity = entry.quantity - date = entry.startDate - foodType = entry.foodType - absorptionTime = entry.absorptionTime - - absorptionTimeWasEdited = true - usesCustomFoodType = true - - shouldBeginEditingQuantity = false - } - } - } - - fileprivate var lastEntryDate: Date? - - fileprivate func updateLastEntryDate() { lastEntryDate = Date() } - - fileprivate var quantity: HKQuantity? { - didSet { - if quantity != oldValue { - updateLastEntryDate() - } - updateContinueButtonEnabled() - } - } - - fileprivate var date = Date() { - didSet { - if date != oldValue { - updateLastEntryDate() - } - updateContinueButtonEnabled() - } - } - - fileprivate var foodType: String? { - didSet { - if foodType != oldValue { - updateLastEntryDate() - } - updateContinueButtonEnabled() - } - } - - fileprivate var absorptionTime: TimeInterval? { - didSet { - if absorptionTime != oldValue { - updateLastEntryDate() - } - updateContinueButtonEnabled() - } - } - - private var selectedDefaultAbsorptionTimeEmoji: String? - - fileprivate var absorptionTimeWasEdited = false - - fileprivate var usesCustomFoodType = false - - private var shouldBeginEditingQuantity = true - - private var shouldBeginEditingFoodType = false - - private var shouldDisplayMissedMealWarning = false { - didSet { - if shouldDisplayMissedMealWarning != oldValue { - if shouldDisplayOverrideEnabledWarning { - self.displayWarningRow(rowType: WarningRow.missedMeal, isAddingRow: shouldDisplayMissedMealWarning) - } else { - self.shouldDisplayWarning = shouldDisplayMissedMealWarning || shouldDisplayOverrideEnabledWarning - } - } - } - } - - private var shouldDisplayOverrideEnabledWarning = false { - didSet { - if shouldDisplayOverrideEnabledWarning != oldValue { - if shouldDisplayMissedMealWarning { - self.displayWarningRow(rowType: WarningRow.override, isAddingRow: shouldDisplayOverrideEnabledWarning) - } else { - self.shouldDisplayWarning = shouldDisplayOverrideEnabledWarning || shouldDisplayMissedMealWarning - } - } - } - } - - private var shouldDisplayWarning = false { - didSet { - if shouldDisplayWarning != oldValue { - self.displayWarning() - } - } - } - - var updatedCarbEntry: NewCarbEntry? { - if let lastEntryDate = lastEntryDate, - let quantity = quantity, - let absorptionTime = absorptionTime ?? defaultAbsorptionTimes?.medium - { - if let o = originalCarbEntry, o.quantity == quantity && o.startDate == date && o.foodType == foodType && o.absorptionTime == absorptionTime { - return nil // No changes were made - } - - return NewCarbEntry( - date: lastEntryDate, - quantity: quantity, - startDate: date, - foodType: foodType ?? selectedDefaultAbsorptionTimeEmoji, - absorptionTime: absorptionTime - ) - } else { - return nil - } - } - - private var isSampleEditable: Bool { - return originalCarbEntry?.createdByCurrentApp != false - } - - private(set) lazy var footerView: SetupTableFooterView = { - let footerView = SetupTableFooterView(frame: .zero) - footerView.primaryButton.addTarget(self, action: #selector(continueButtonPressed), for: .touchUpInside) - footerView.primaryButton.isEnabled = quantity != nil && quantity!.doubleValue(for: preferredCarbUnit) > 0 - return footerView - }() - - private var lastContentHeight: CGFloat = 0 - - override func createChartsManager() -> ChartsManager { - // Consider including a chart on this screen to demonstrate how absorption time affects prediction - ChartsManager(colors: .primary, settings: .default, charts: [], traitCollection: traitCollection) - } - - override func glucoseUnitDidChange() { - // Consider including a chart on this screen to demonstrate how absorption time affects prediction - } - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 44 - tableView.register(DateAndDurationTableViewCell.nib(), forCellReuseIdentifier: DateAndDurationTableViewCell.className) - tableView.register(DateAndDurationSteppableTableViewCell.nib(), forCellReuseIdentifier: DateAndDurationSteppableTableViewCell.className) - - if originalCarbEntry != nil { - title = NSLocalizedString("carb-entry-title-edit", value: "Edit Carb Entry", comment: "The title of the view controller to edit an existing carb entry") - } else { - title = NSLocalizedString("carb-entry-title-add", value: "Add Carb Entry", comment: "The title of the view controller to create a new carb entry") - } - - navigationItem.rightBarButtonItem = UIBarButtonItem(title: footerView.primaryButton.titleLabel?.text, style: .plain, target: self, action: #selector(continueButtonPressed)) - navigationItem.rightBarButtonItem?.isEnabled = false - - // Sets text for back button on bolus screen - navigationItem.backBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Carb Entry", comment: "Back button text for bolus screen to return to carb entry screen"), style: .plain, target: nil, action: nil) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if shouldBeginEditingQuantity, let cell = tableView.cellForRow(at: IndexPath(row: DetailsRow.value.rawValue, section: Sections.indexForDetailsSection(displayWarningSection: shouldDisplayWarning))) as? DecimalTextFieldTableViewCell { - shouldBeginEditingQuantity = false - cell.textField.becomeFirstResponder() - } - - // check if warning should be displayed - updateDisplayOverrideEnabledWarning() - - // monitor loop updates - notificationObservers += [ - NotificationCenter.default.addObserver(forName: .LoopDataUpdated, object: deviceManager.loopManager, queue: nil) { [weak self] _ in - self?.updateDisplayOverrideEnabledWarning() - } - ] - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - // Reposition footer view if necessary - if tableView.contentSize.height != lastContentHeight { - lastContentHeight = tableView.contentSize.height - tableView.tableFooterView = nil - - let footerSize = footerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.size.width, height: UIView.layoutFittingCompressedSize.height)) - footerView.frame.size = footerSize - tableView.tableFooterView = footerView - } - } - - private var foodKeyboard: EmojiInputController! - - private func updateDisplayOverrideEnabledWarning() { - DispatchQueue.main.async { - if let managerSettings = self.deviceManager?.settings { - if !managerSettings.scheduleOverrideEnabled(at: Date()) { - self.shouldDisplayOverrideEnabledWarning = false - } else if let overrideSettings = managerSettings.scheduleOverride?.settings { - self.shouldDisplayOverrideEnabledWarning = overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 - } - } - } - } - - private func displayWarning() { - tableView.beginUpdates() - - if shouldDisplayWarning { - tableView.insertSections([Sections.warning.rawValue], with: .top) - } else { - tableView.deleteSections([Sections.warning.rawValue], with: .top) - } - - tableView.endUpdates() - } - - private func displayWarningRow(rowType: WarningRow, isAddingRow: Bool = true ) { - if shouldDisplayWarning { - tableView.beginUpdates() - - // If the missed meal warning is shown, use the positional index of the given row type. - let rowIndex = shouldDisplayMissedMealWarning ? rowType.rawValue : 0 - - if isAddingRow { - tableView.insertRows(at: [IndexPath(row: rowIndex, section: Sections.warning.rawValue)], with: UITableView.RowAnimation.top) - } else { - tableView.deleteRows(at: [IndexPath(row: rowIndex, section: Sections.warning.rawValue)], with: UITableView.RowAnimation.top) - } - - tableView.endUpdates() - } - } - - // MARK: - Table view data source - fileprivate enum Sections: Int, CaseIterable { - case warning - case details - - static func indexForDetailsSection(displayWarningSection: Bool) -> Int { - return displayWarningSection ? Sections.details.rawValue : Sections.details.rawValue - 1 - } - - static func numberOfSections(displayWarningSection: Bool) -> Int { - return displayWarningSection ? Sections.allCases.count : Sections.allCases.count - 1 - } - - static func section(for indexPath: IndexPath, displayWarningSection: Bool) -> Int { - return displayWarningSection ? indexPath.section : indexPath.section + 1 - } - - static func numberOfRows(for section: Int, displayMissedMealWarning: Bool, displayOverrideWarning: Bool) -> Int { - if section == Sections.warning.rawValue && (displayMissedMealWarning || displayOverrideWarning) { - return displayMissedMealWarning && displayOverrideWarning ? WarningRow.allCases.count : WarningRow.allCases.count - 1 - } - - return DetailsRow.allCases.count - } - - static func footer(for section: Int, displayWarningSection: Bool) -> String? { - if section == Sections.warning.rawValue && displayWarningSection { - return nil - } - - return NSLocalizedString("Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact.", comment: "Carb entry section footer text explaining absorption time") - } - - static func headerHeight(for section: Int, displayWarningSection: Bool) -> CGFloat { - return 8 - } - - static func footerHeight(for section: Int, displayWarningSection: Bool) -> CGFloat { - if section == Sections.warning.rawValue && displayWarningSection { - return 1 - } - - return UITableView.automaticDimension - } - } - - fileprivate enum DetailsRow: Int, CaseIterable { - case value - case date - case foodType - case absorptionTime - } - - fileprivate enum WarningRow: Int, CaseIterable { - case missedMeal - case override - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return Sections.numberOfSections(displayWarningSection: shouldDisplayWarning) - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Sections.numberOfRows(for: section, displayMissedMealWarning: shouldDisplayMissedMealWarning, displayOverrideWarning: shouldDisplayOverrideEnabledWarning) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch Sections(rawValue: Sections.section(for: indexPath, displayWarningSection: shouldDisplayWarning))! { - case .warning: - let cell: UITableViewCell - // if no missed meal warning should be shown OR if the given indexPath is for the override warning row, return the override warning cell. - if !shouldDisplayMissedMealWarning || WarningRow(rawValue: indexPath.row)! == .override { - if let existingCell = tableView.dequeueReusableCell(withIdentifier: "CarbEntryOverrideEnabledWarningCell") { - cell = existingCell - } else { - cell = UITableViewCell(style: .default, reuseIdentifier: "CarbEntryOverrideEnabledWarningCell") - } - - cell.imageView?.image = UIImage(systemName: "exclamationmark.triangle.fill") - cell.imageView?.tintColor = .warning - cell.textLabel?.numberOfLines = 0 - cell.textLabel?.text = NSLocalizedString("An active override is modifying your carb ratio and insulin sensitivity. If you don't want this to affect your bolus calculation and projected glucose, consider turning off the override.", comment: "Warning to ensure the carb entry is accurate during an override") - cell.textLabel?.font = UIFont.preferredFont(forTextStyle: .caption1) - cell.textLabel?.textColor = .secondaryLabel - cell.isUserInteractionEnabled = false - } else { - if let existingCell = tableView.dequeueReusableCell(withIdentifier: "CarbEntryAccuracyWarningCell") { - cell = existingCell - } else { - cell = UITableViewCell(style: .default, reuseIdentifier: "CarbEntryAccuracyWarningCell") - } - - cell.imageView?.image = UIImage(systemName: "exclamationmark.triangle.fill") - cell.imageView?.tintColor = .destructive - cell.textLabel?.numberOfLines = 0 - cell.textLabel?.text = NSLocalizedString("Loop has detected an missed meal and estimated its size. Edit the carb amount to match the amount of any carbs you may have eaten.", comment: "Warning displayed when user is adding a meal from an missed meal notification") - cell.textLabel?.font = UIFont.preferredFont(forTextStyle: .caption1) - cell.textLabel?.textColor = .secondaryLabel - cell.isUserInteractionEnabled = false - } - return cell - case .details: - switch DetailsRow(rawValue: indexPath.row)! { - case .value: - let cell = tableView.dequeueReusableCell(withIdentifier: DecimalTextFieldTableViewCell.className) as! DecimalTextFieldTableViewCell - - if let quantity = quantity { - cell.number = NSNumber(value: quantity.doubleValue(for: preferredCarbUnit)) - } - cell.textField.isEnabled = isSampleEditable - cell.unitLabel?.text = String(describing: preferredCarbUnit) - cell.delegate = self - - return cell - case .date: - let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationSteppableTableViewCell.className) as! DateAndDurationSteppableTableViewCell - - cell.titleLabel.text = NSLocalizedString("Time", comment: "Title of the carb entry date picker cell") - cell.datePicker.isEnabled = isSampleEditable - cell.datePicker.datePickerMode = .dateAndTime - #if swift(>=5.2) - if #available(iOS 14.0, *) { - cell.datePicker.preferredDatePickerStyle = .wheels - } - #endif - cell.datePicker.maximumDate = date.addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) - cell.datePicker.minimumDate = date.addingTimeInterval(LoopConstants.maxCarbEntryPastTime) - cell.datePicker.minuteInterval = 1 - cell.date = date - cell.delegate = self - - return cell - case .foodType: - if usesCustomFoodType { - let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.className, for: indexPath) as! TextFieldTableViewCell - - cell.textField.text = foodType - cell.delegate = self - - if let textField = cell.textField as? CustomInputTextField { - if foodKeyboard == nil { - foodKeyboard = CarbAbsorptionInputController() - foodKeyboard.delegate = self - } - - textField.customInput = foodKeyboard - } - - return cell - } else { - let cell = tableView.dequeueReusableCell(withIdentifier: FoodTypeShortcutCell.className, for: indexPath) as! FoodTypeShortcutCell - - if absorptionTime == nil { - cell.selectionState = .medium - } - - selectedDefaultAbsorptionTimeEmoji = cell.selectedEmoji - cell.delegate = self - - return cell - } - case .absorptionTime: - let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className) as! DateAndDurationTableViewCell - - cell.titleLabel.text = NSLocalizedString("Absorption Time", comment: "Title of the carb entry absorption time cell") - cell.datePicker.isEnabled = isSampleEditable - cell.datePicker.datePickerMode = .countDownTimer - cell.datePicker.minuteInterval = Int(absorptionTimePickerInterval.minutes) - - if let duration = absorptionTime ?? defaultAbsorptionTimes?.medium { - cell.duration = duration - } - - cell.maximumDuration = maxAbsorptionTime - cell.delegate = self - - return cell - } - } - } - - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - switch Sections(rawValue: Sections.section(for: indexPath, displayWarningSection: shouldDisplayWarning)) { - case .details: - switch DetailsRow(rawValue: indexPath.row)! { - case .value, .date: - break - case .foodType: - if usesCustomFoodType, shouldBeginEditingFoodType, let cell = cell as? TextFieldTableViewCell { - shouldBeginEditingFoodType = false - cell.textField.becomeFirstResponder() - } - case .absorptionTime: - break - } - default: - break - } - } - - override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { - return Sections.footer(for: section, displayWarningSection: shouldDisplayWarning) - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return Sections.headerHeight(for: section, displayWarningSection: shouldDisplayWarning) - } - - override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return Sections.footerHeight(for: section, displayWarningSection: shouldDisplayWarning) - } - - // MARK: - UITableViewDelegate - - override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - tableView.endEditing(false) - tableView.beginUpdates() - hideDatePickerCells(excluding: indexPath) - return indexPath - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch tableView.cellForRow(at: indexPath) { - case is FoodTypeShortcutCell: - usesCustomFoodType = true - shouldBeginEditingFoodType = true - tableView.reloadRows(at: [IndexPath(row: DetailsRow.foodType.rawValue, section: Sections.indexForDetailsSection(displayWarningSection: shouldDisplayWarning))], with: .none) - default: - break - } - - tableView.endUpdates() - tableView.deselectRow(at: indexPath, animated: true) - } - - // MARK: - Navigation - - override func restoreUserActivityState(_ activity: NSUserActivity) { - if let entry = activity.newCarbEntry { - lastEntryDate = entry.date - quantity = entry.quantity - date = entry.startDate - - if let foodType = entry.foodType { - self.foodType = foodType - usesCustomFoodType = true - } - - if let absorptionTime = entry.absorptionTime { - self.absorptionTime = absorptionTime - absorptionTimeWasEdited = true - } - - if activity.entryisMissedMeal { - shouldDisplayMissedMealWarning = true - } - } - } - - @objc private func continueButtonPressed() { - tableView.endEditing(true) - guard validateInput() else { - return - } - continueToBolus() - } - - private func continueToBolus() { - - guard let updatedEntry = updatedCarbEntry else { - return - } - - let viewModel = BolusEntryViewModel( - delegate: deviceManager, - screenWidth: UIScreen.main.bounds.width, - originalCarbEntry: originalCarbEntry, - potentialCarbEntry: updatedEntry, - selectedCarbAbsorptionTimeEmoji: selectedDefaultAbsorptionTimeEmoji - ) - Task { - await viewModel.generateRecommendationAndStartObserving() - } - - viewModel.analyticsServicesManager = deviceManager.analyticsServicesManager - - let bolusEntryView = BolusEntryView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) - - // After confirming a bolus, pop back to this controller's predecessor, i.e. all the way back out of the carb flow. - let predecessorViewControllerType = (navigationController?.viewControllers.dropLast().last).map { type(of: $0) } ?? UIViewController.self - let hostingController = DismissibleHostingController( - rootView: bolusEntryView, - dismissalMode: originalCarbEntry == nil ? .modalDismiss : .pop(to: predecessorViewControllerType) - ) - show(hostingController, sender: footerView.primaryButton) - deviceManager.analyticsServicesManager.didDisplayBolusScreen() - } - - private func validateInput() -> Bool { - guard let absorptionTime = absorptionTime ?? defaultAbsorptionTimes?.medium else { - return false - } - guard absorptionTime <= maxAbsorptionTime else { - showAbsorptionTimeValidationWarning(for: self, maxAbsorptionTime: maxAbsorptionTime) - return false - } - - guard let quantity = quantity, quantity.doubleValue(for: preferredCarbUnit) > 0 else { return false } - guard quantity.compare(maxCarbEntryQuantity) != .orderedDescending else { - showMaxQuantityValidationWarning(for: self, maxQuantityGrams: maxCarbEntryQuantity.doubleValue(for: .gram())) - return false - } - - let enteredGrams = quantity.doubleValue(for: .gram()) - - if (enteredGrams > warningCarbEntryQuantity.doubleValue(for: .gram())) { - showWarningQuantityValidationWarning(for: self, enteredGrams: enteredGrams) { - self.continueToBolus() - } - return false - } - return true - } - - private func updateContinueButtonEnabled() { - let hasValidQuantity = quantity != nil && quantity!.doubleValue(for: preferredCarbUnit) > 0 - let haveChangesBeenMade = updatedCarbEntry != nil - - let readyToContinue = hasValidQuantity && haveChangesBeenMade - - footerView.primaryButton.isEnabled = readyToContinue - navigationItem.rightBarButtonItem?.isEnabled = readyToContinue - } - - // Alerts - private lazy var dismissActionTitle = NSLocalizedString("com.loudnate.LoopKit.errorAlertActionTitle", value: "OK", comment: "The title of the action used to dismiss an error alert") - - public func showAbsorptionTimeValidationWarning(for viewController: UIViewController, maxAbsorptionTime: TimeInterval) { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.minute] - formatter.unitsStyle = .full - - let message = String( - format: NSLocalizedString("The maximum absorption time is %@", comment: "Alert body displayed absorption time greater than max (1: maximum absorption time)"), - formatter.string(from: maxAbsorptionTime) ?? String(describing: maxAbsorptionTime)) - let validationTitle = NSLocalizedString("Maximum Duration Exceeded", comment: "Alert title when maximum duration exceeded.") - let alert = UIAlertController(title: validationTitle, message: message, preferredStyle: .alert) - - let action = UIAlertAction(title: dismissActionTitle, style: .default) - alert.addAction(action) - alert.preferredAction = action - - viewController.present(alert, animated: true) - } - - public func showWarningQuantityValidationWarning(for viewController: UIViewController, enteredGrams: Double, didConfirm: @escaping () -> Void) { - let warningTitle = NSLocalizedString("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered") - - let message = String( - format: NSLocalizedString("Did you intend to enter %1$@ grams as the amount of carbohydrates for this meal?", comment: "Alert body when entered carbohydrates is greater than threshold (1: entered quantity in grams)"), - NumberFormatter.localizedString(from: NSNumber(value: enteredGrams), number: .none) - ) - let alert = UIAlertController(title: warningTitle, message: message, preferredStyle: .alert) - - let editButtonText = NSLocalizedString("No, edit amount", comment: "The title of the action used when rejecting the the amount of carbohydrates entered.") - let editAction = UIAlertAction(title: editButtonText, style: .default) - alert.addAction(editAction) - - let confirmButtonText = NSLocalizedString("Yes", comment: "The title of the action used when confirming entered amount of carbohydrates.") - let confirm = UIAlertAction(title: confirmButtonText, style: .default) {_ in - didConfirm(); - } - alert.addAction(confirm) - alert.preferredAction = confirm - - viewController.present(alert, animated: true) - } - - public func showMaxQuantityValidationWarning(for viewController: UIViewController, maxQuantityGrams: Double) { - let errorTitle = NSLocalizedString("Input Maximum Exceeded", comment: "Title of the alert when carb input maximum was exceeded.") - let message = String( - format: NSLocalizedString("The maximum allowed amount is %@ grams.", comment: "Alert body displayed for quantity greater than max (1: maximum quantity in grams)"), - NumberFormatter.localizedString(from: NSNumber(value: maxQuantityGrams), number: .none) - ) - let alert = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert) - - let action = UIAlertAction(title: dismissActionTitle, style: .default) - alert.addAction(action) - alert.preferredAction = action - - viewController.present(alert, animated: true) - } -} - - -extension CarbEntryViewController: TextFieldTableViewCellDelegate { - func textFieldTableViewCellDidBeginEditing(_ cell: TextFieldTableViewCell) { - // Collapse any date picker cells to save space - tableView.beginUpdates() - hideDatePickerCells() - tableView.endUpdates() - } - - func textFieldTableViewCellDidChangeEditing(_ cell: TextFieldTableViewCell) { - guard let row = tableView.indexPath(for: cell)?.row else { return } - - switch DetailsRow(rawValue: row) { - case .value?: - if let cell = cell as? DecimalTextFieldTableViewCell, let number = cell.number { - quantity = HKQuantity(unit: preferredCarbUnit, doubleValue: number.doubleValue) - } else { - quantity = nil - } - case .foodType?: - foodType = cell.textField.text - default: - break - } - } - - func textFieldTableViewCellDidEndEditing(_ cell: TextFieldTableViewCell) { - textFieldTableViewCellDidChangeEditing(cell) - } -} - -extension CarbEntryViewController: DatePickerTableViewCellDelegate { - func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) { - guard let row = tableView.indexPath(for: cell)?.row else { return } - - switch DetailsRow(rawValue: row) { - case .date?: - date = cell.date - case .absorptionTime?: - absorptionTime = cell.duration - absorptionTimeWasEdited = true - default: - break - } - } -} - - -extension CarbEntryViewController: FoodTypeShortcutCellDelegate { - func foodTypeShortcutCellDidUpdateSelection(_ cell: FoodTypeShortcutCell) { - var absorptionTime: TimeInterval? - - switch cell.selectionState { - case .fast: - absorptionTime = defaultAbsorptionTimes?.fast - case .medium: - absorptionTime = defaultAbsorptionTimes?.medium - case .slow: - absorptionTime = defaultAbsorptionTimes?.slow - case .custom: - tableView.beginUpdates() - usesCustomFoodType = true - shouldBeginEditingFoodType = true - tableView.reloadRows(at: [IndexPath(row: DetailsRow.foodType.rawValue, section: Sections.indexForDetailsSection(displayWarningSection: shouldDisplayWarning))], with: .fade) - tableView.endUpdates() - } - - if let absorptionTime = absorptionTime { - self.absorptionTime = absorptionTime - - if let cell = tableView.cellForRow(at: IndexPath(row: DetailsRow.absorptionTime.rawValue, section: Sections.indexForDetailsSection(displayWarningSection: shouldDisplayWarning))) as? DateAndDurationTableViewCell { - cell.duration = absorptionTime - } - } - - selectedDefaultAbsorptionTimeEmoji = cell.selectedEmoji - } -} - - -extension CarbEntryViewController: EmojiInputControllerDelegate { - func emojiInputControllerDidAdvanceToStandardInputMode(_ controller: EmojiInputController) { - if let cell = tableView.cellForRow(at: IndexPath(row: DetailsRow.foodType.rawValue, section: Sections.indexForDetailsSection(displayWarningSection: shouldDisplayWarning))) as? TextFieldTableViewCell, let textField = cell.textField as? CustomInputTextField, textField.customInput != nil { - let customInput = textField.customInput - textField.customInput = nil - textField.resignFirstResponder() - textField.becomeFirstResponder() - textField.customInput = customInput - } - } - - func emojiInputControllerDidSelectItemInSection(_ section: Int) { - guard !absorptionTimeWasEdited, section < orderedAbsorptionTimes.count else { - return - } - - if absorptionTime == nil { - // only adjust the absorption time if it wasn't already set. - absorptionTime = orderedAbsorptionTimes[section] - - if let cell = tableView.cellForRow(at: IndexPath(row: DetailsRow.absorptionTime.rawValue, section: Sections.indexForDetailsSection(displayWarningSection: shouldDisplayWarning))) as? DateAndDurationTableViewCell { - cell.duration = orderedAbsorptionTimes[section] - } - } - } -} - -extension DateAndDurationTableViewCell: NibLoadable {} - -extension DateAndDurationSteppableTableViewCell: NibLoadable {} diff --git a/Loop/View Controllers/RootNavigationController.swift b/Loop/View Controllers/RootNavigationController.swift index 910ada7c79..87b04d5e93 100644 --- a/Loop/View Controllers/RootNavigationController.swift +++ b/Loop/View Controllers/RootNavigationController.swift @@ -40,24 +40,6 @@ class RootNavigationController: UINavigationController { if viewControllers.count > 1 { popToRootViewController(animated: false) } - case NSUserActivity.newCarbEntryActivityType: - if let navVC = presentedViewController as? UINavigationController { - if let carbVC = navVC.topViewController as? CarbEntryViewController { - carbVC.restoreUserActivityState(activity) - return - } else { - dismiss(animated: false, completion: nil) - } - } - - if let carbVC = topViewController as? CarbAbsorptionViewController { - carbVC.restoreUserActivityState(activity) - return - } else if viewControllers.count > 1 { - popToRootViewController(animated: false) - } - - fallthrough default: statusTableViewController.restoreUserActivityState(activity) } diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift new file mode 100644 index 0000000000..44c7a83150 --- /dev/null +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -0,0 +1,73 @@ +// +// FavoriteFoodDetailView.swift +// Loop +// +// Created by Noah Brauner on 8/2/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import HealthKit + +public struct FavoriteFoodDetailView: View { + let food: StoredFavoriteFood? + let onFoodDelete: (StoredFavoriteFood) -> Void + + @State private var isConfirmingDelete = false + + let carbFormatter: QuantityFormatter + let absorptionTimeFormatter: DateComponentsFormatter + let preferredCarbUnit: HKUnit + + public init(food: StoredFavoriteFood?, onFoodDelete: @escaping (StoredFavoriteFood) -> Void, isConfirmingDelete: Bool = false, carbFormatter: QuantityFormatter, absorptionTimeFormatter: DateComponentsFormatter, preferredCarbUnit: HKUnit = HKUnit.gram()) { + self.food = food + self.onFoodDelete = onFoodDelete + self.isConfirmingDelete = isConfirmingDelete + self.carbFormatter = carbFormatter + self.absorptionTimeFormatter = absorptionTimeFormatter + self.preferredCarbUnit = preferredCarbUnit + } + + public var body: some View { + if let food { + List { + Section("Information") { + VStack(spacing: 16) { + let rows: [(field: String, value: String)] = [ + ("Name", food.name), + ("Carb Quantity", food.carbsString(formatter: carbFormatter)), + ("Food Type", food.foodType), + ("Absorption Time", food.absorptionTimeString(formatter: absorptionTimeFormatter)) + ] + ForEach(rows, id: \.field) { row in + HStack { + Text(row.field) + .font(.subheadline) + Spacer() + Text(row.value) + .font(.subheadline) + } + } + } + } + .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) + + Button(role: .destructive, action: { isConfirmingDelete.toggle() }) { + Text("Delete Food") + .frame(maxWidth: .infinity, alignment: .center) // Align text in center + } + } + .alert(isPresented: $isConfirmingDelete) { + Alert( + title: Text("Delete “\(food.name)”?"), + message: Text("Are you sure you want to delete this food?"), + primaryButton: .cancel(), + secondaryButton: .destructive(Text("Delete"), action: { onFoodDelete(food) }) + ) + } + .insetGroupedListStyle() + .navigationTitle(food.title) + } + } +} From 05974e775adb96765b3b58754b96bd5063695ace Mon Sep 17 00:00:00 2001 From: SwiftlyNoah Date: Mon, 14 Aug 2023 10:50:57 -0400 Subject: [PATCH 19/22] UI test fix --- Loop/View Controllers/StatusTableViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index cdac208d13..74b49790aa 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -291,7 +291,7 @@ final class StatusTableViewController: LoopChartsTableViewController { private func updateToolbarItems() { let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true - toolbarItems![0].accessibilityLabel = NSLocalizedString("Enter Carbs", comment: "The label of the carb entry button") + toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") toolbarItems![0].isEnabled = isPumpOnboarded toolbarItems![0].tintColor = UIColor.carbTintColor toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") From f6135aaada943a20cebc1993e0158054716def17 Mon Sep 17 00:00:00 2001 From: Noah Brauner <66573062+SwiftlyNoah@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:00:31 -0400 Subject: [PATCH 20/22] Asset override for Widgets (#589) --- .../AppIcon.appiconset/Contents.json | 98 ------------------ .../Contents.json | 0 .../WidgetBackground.colorset/Contents.json | 0 .../Contents.json | 0 .../bolus.imageset/Contents.json | 0 .../bolus.imageset/bolus.pdf | Bin .../carbs.imageset/Contents.json | 0 .../carbs.imageset/Meal.pdf | Bin .../premeal.imageset/Contents.json | 0 .../premeal.imageset/Pre-Meal.pdf | Bin .../workout.imageset/Contents.json | 0 .../workout.imageset/workout.pdf | Bin .../Contents.json | 5 - .../DerivedAssetsBase.xcassets/Contents.json | 6 ++ .../fresh.colorset/Contents.json | 0 .../glucose.colorset/Contents.json | 0 .../insulin.colorset/Contents.json | 0 .../warning.colorset/Contents.json | 0 Loop.xcodeproj/project.pbxproj | 16 ++- 19 files changed, 18 insertions(+), 107 deletions(-) delete mode 100644 Loop Widget Extension/Bootstrap/Assets.xcassets/AppIcon.appiconset/Contents.json rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/WidgetBackground.colorset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/WidgetSecondaryBackground.colorset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/bolus.imageset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/bolus.imageset/bolus.pdf (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/carbs.imageset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/carbs.imageset/Meal.pdf (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/premeal.imageset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/premeal.imageset/Pre-Meal.pdf (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/workout.imageset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DefaultAssets.xcassets}/workout.imageset/workout.pdf (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets/AccentColor.colorset => DerivedAssets.xcassets}/Contents.json (51%) create mode 100644 Loop Widget Extension/DerivedAssetsBase.xcassets/Contents.json rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DerivedAssetsBase.xcassets}/fresh.colorset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DerivedAssetsBase.xcassets}/glucose.colorset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DerivedAssetsBase.xcassets}/insulin.colorset/Contents.json (100%) rename Loop Widget Extension/{Bootstrap/Assets.xcassets => DerivedAssetsBase.xcassets}/warning.colorset/Contents.json (100%) diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/AppIcon.appiconset/Contents.json b/Loop Widget Extension/Bootstrap/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb1a..0000000000 --- a/Loop Widget Extension/Bootstrap/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/WidgetBackground.colorset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/WidgetBackground.colorset/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/WidgetBackground.colorset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/WidgetSecondaryBackground.colorset/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/WidgetSecondaryBackground.colorset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/WidgetSecondaryBackground.colorset/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/WidgetSecondaryBackground.colorset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/bolus.imageset/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/bolus.imageset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/bolus.imageset/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/bolus.imageset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/bolus.imageset/bolus.pdf b/Loop Widget Extension/DefaultAssets.xcassets/bolus.imageset/bolus.pdf similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/bolus.imageset/bolus.pdf rename to Loop Widget Extension/DefaultAssets.xcassets/bolus.imageset/bolus.pdf diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/carbs.imageset/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/carbs.imageset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/carbs.imageset/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/carbs.imageset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/carbs.imageset/Meal.pdf b/Loop Widget Extension/DefaultAssets.xcassets/carbs.imageset/Meal.pdf similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/carbs.imageset/Meal.pdf rename to Loop Widget Extension/DefaultAssets.xcassets/carbs.imageset/Meal.pdf diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/premeal.imageset/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/premeal.imageset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/premeal.imageset/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/premeal.imageset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/premeal.imageset/Pre-Meal.pdf b/Loop Widget Extension/DefaultAssets.xcassets/premeal.imageset/Pre-Meal.pdf similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/premeal.imageset/Pre-Meal.pdf rename to Loop Widget Extension/DefaultAssets.xcassets/premeal.imageset/Pre-Meal.pdf diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/workout.imageset/Contents.json b/Loop Widget Extension/DefaultAssets.xcassets/workout.imageset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/workout.imageset/Contents.json rename to Loop Widget Extension/DefaultAssets.xcassets/workout.imageset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/workout.imageset/workout.pdf b/Loop Widget Extension/DefaultAssets.xcassets/workout.imageset/workout.pdf similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/workout.imageset/workout.pdf rename to Loop Widget Extension/DefaultAssets.xcassets/workout.imageset/workout.pdf diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/AccentColor.colorset/Contents.json b/Loop Widget Extension/DerivedAssets.xcassets/Contents.json similarity index 51% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/AccentColor.colorset/Contents.json rename to Loop Widget Extension/DerivedAssets.xcassets/Contents.json index eb87897008..73c00596a7 100644 --- a/Loop Widget Extension/Bootstrap/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Loop Widget Extension/DerivedAssets.xcassets/Contents.json @@ -1,9 +1,4 @@ { - "colors" : [ - { - "idiom" : "universal" - } - ], "info" : { "author" : "xcode", "version" : 1 diff --git a/Loop Widget Extension/DerivedAssetsBase.xcassets/Contents.json b/Loop Widget Extension/DerivedAssetsBase.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Loop Widget Extension/DerivedAssetsBase.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/fresh.colorset/Contents.json b/Loop Widget Extension/DerivedAssetsBase.xcassets/fresh.colorset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/fresh.colorset/Contents.json rename to Loop Widget Extension/DerivedAssetsBase.xcassets/fresh.colorset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/glucose.colorset/Contents.json b/Loop Widget Extension/DerivedAssetsBase.xcassets/glucose.colorset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/glucose.colorset/Contents.json rename to Loop Widget Extension/DerivedAssetsBase.xcassets/glucose.colorset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/insulin.colorset/Contents.json b/Loop Widget Extension/DerivedAssetsBase.xcassets/insulin.colorset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/insulin.colorset/Contents.json rename to Loop Widget Extension/DerivedAssetsBase.xcassets/insulin.colorset/Contents.json diff --git a/Loop Widget Extension/Bootstrap/Assets.xcassets/warning.colorset/Contents.json b/Loop Widget Extension/DerivedAssetsBase.xcassets/warning.colorset/Contents.json similarity index 100% rename from Loop Widget Extension/Bootstrap/Assets.xcassets/warning.colorset/Contents.json rename to Loop Widget Extension/DerivedAssetsBase.xcassets/warning.colorset/Contents.json diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 1961558d07..a1a9ee7c75 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -15,13 +15,15 @@ 1452F4A92A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */; }; 1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */; }; 1452F4AD2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */; }; + 147EFE8E2A8BCC5500272438 /* DefaultAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */; }; + 147EFE902A8BCD8000272438 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */; }; + 147EFE922A8BCD8A00272438 /* DerivedAssetsBase.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */; }; 149A28BD2A853E6C00052EDF /* CarbEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */; }; 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; - 14B1736528AED9EE006CCD7C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14B1736428AED9EE006CCD7C /* Assets.xcassets */; }; 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 14B1737228AEDBF6006CCD7C /* BasalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736E28AEDBF6006CCD7C /* BasalView.swift */; }; 14B1737328AEDBF6006CCD7C /* SystemStatusWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736F28AEDBF6006CCD7C /* SystemStatusWidget.swift */; }; @@ -751,7 +753,9 @@ 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - 14B1736428AED9EE006CCD7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DefaultAssets.xcassets; sourceTree = ""; }; + 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = ""; }; + 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssetsBase.xcassets; sourceTree = ""; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 14B1736D28AEDA63006CCD7C /* LoopWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoopWidgetExtension.entitlements; sourceTree = ""; }; 14B1736E28AEDBF6006CCD7C /* BasalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalView.swift; sourceTree = ""; }; @@ -1820,6 +1824,9 @@ 14B1736128AED9EC006CCD7C /* Loop Widget Extension */ = { isa = PBXGroup; children = ( + 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */, + 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */, + 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */, 84AA81D42A4A2813000B658B /* Bootstrap */, 84AA81D12A4A2778000B658B /* Components */, 84AA81D92A4A2966000B658B /* Helpers */, @@ -2509,7 +2516,6 @@ C116134A2983096D00777E7C /* Localizable.strings */, C11613472983096D00777E7C /* InfoPlist.strings */, 14B1736D28AEDA63006CCD7C /* LoopWidgetExtension.entitlements */, - 14B1736428AED9EE006CCD7C /* Assets.xcassets */, 14B1736628AED9EE006CCD7C /* Info.plist */, ); path = Bootstrap; @@ -3336,9 +3342,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 14B1736528AED9EE006CCD7C /* Assets.xcassets in Resources */, C116134C2983096D00777E7C /* Localizable.strings in Resources */, + 147EFE8E2A8BCC5500272438 /* DefaultAssets.xcassets in Resources */, C11613492983096D00777E7C /* InfoPlist.strings in Resources */, + 147EFE902A8BCD8000272438 /* DerivedAssets.xcassets in Resources */, + 147EFE922A8BCD8A00272438 /* DerivedAssetsBase.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 3739ae0c81219b494156ed8a30f92f1995ac7512 Mon Sep 17 00:00:00 2001 From: Noah Brauner <66573062+SwiftlyNoah@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:08:46 -0400 Subject: [PATCH 21/22] Remove unused code (#590) --- Loop/Extensions/UIImage.swift | 4 ---- LoopUI/Extensions/UIColor.swift | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Loop/Extensions/UIImage.swift b/Loop/Extensions/UIImage.swift index 315f90a5d5..908f0c965a 100644 --- a/Loop/Extensions/UIImage.swift +++ b/Loop/Extensions/UIImage.swift @@ -30,10 +30,6 @@ extension UIImage { return suffix } - - static func presetsImage(selected: Bool) -> UIImage? { - return UIImage(named: selected ? "presets-selected" : "presets") - } static func preMealImage(selected: Bool) -> UIImage? { return UIImage(named: selected ? "Pre-Meal Selected" : "Pre-Meal") diff --git a/LoopUI/Extensions/UIColor.swift b/LoopUI/Extensions/UIColor.swift index ba1c5700f8..db638084f8 100644 --- a/LoopUI/Extensions/UIColor.swift +++ b/LoopUI/Extensions/UIColor.swift @@ -18,8 +18,6 @@ extension UIColor { @nonobjc static let insulin = UIColor(named: "insulin") ?? systemOrange - @nonobjc static let preset = UIColor(named: "presets") ?? systemBlue - // The loopAccent color is intended to be use as the app accent color. @nonobjc public static let loopAccent = UIColor(named: "accent") ?? systemBlue @@ -52,8 +50,6 @@ extension UIColor { @nonobjc public static let insulinTintColor = insulin - @nonobjc public static let presetTintColor = preset - @nonobjc public static let pumpStatusNormal = insulin @nonobjc public static let staleColor = critical From 140c2dbe8800b62e18d059d4e8446a552f0500ed Mon Sep 17 00:00:00 2001 From: Noah Brauner <66573062+SwiftlyNoah@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:39:01 -0400 Subject: [PATCH 22/22] update gitignore (#591) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f238f37c9f..2d772d5f67 100644 --- a/.gitignore +++ b/.gitignore @@ -77,9 +77,12 @@ VersionOverride.xcconfig Loop/DerivedAssets.xcassets/* WatchApp/DerivedAssets.xcassets/* +Loop\ Widget\ Extension/DerivedAssets.xcassets/* # ...except, keep Contents.json !Loop/DerivedAssets.xcassets/Contents.json !WatchApp/DerivedAssets.xcassets/Contents.json +!Loop\ Widget\ Extension/DerivedAssets.xcassets/Contents.json Loop/DerivedAssetsOverride.xcassets WatchApp/DerivedAssetsOverride.xcassets +Loop\ Widget\ Extension/DerivedAssetsOverride.xcassets