Skip to content

Commit 04980c5

Browse files
committed
[LOOP-4690] Add/Edit/Detail for favorite foods
1 parent 86404f7 commit 04980c5

File tree

5 files changed

+325
-3
lines changed

5 files changed

+325
-3
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; };
1313
142CB7592A60BF2E0075748A /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB7582A60BF2E0075748A /* EditMode.swift */; };
1414
142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */; };
15+
1452F4A92A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */; };
16+
1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */; };
17+
1452F4AD2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */; };
1518
1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; };
1619
14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; };
1720
14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; };
@@ -735,6 +738,9 @@
735738
142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = "<group>"; };
736739
14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SmallStatusWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
737740
14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
741+
1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodViewModel.swift; sourceTree = "<group>"; };
742+
1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditFavoriteFoodView.swift; sourceTree = "<group>"; };
743+
1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowAbsorptionTimeWorksView.swift; sourceTree = "<group>"; };
738744
14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
739745
14B1736428AED9EE006CCD7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
740746
14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -2215,6 +2221,7 @@
22152221
43F5C2CF1B92A2ED003EB13D /* Views */ = {
22162222
isa = PBXGroup;
22172223
children = (
2224+
1452F4AA2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift */,
22182225
B4001CED28CBBC82002FB414 /* AlertManagementView.swift */,
22192226
897A5A9524C2175B00C4E71D /* BolusEntryView.swift */,
22202227
C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */,
@@ -2225,6 +2232,7 @@
22252232
C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */,
22262233
142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */,
22272234
43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */,
2235+
1452F4AC2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift */,
22282236
B43CF07D29434EC4008A520B /* HowMuteAlertWorkView.swift */,
22292237
430D85881F44037000AF2D4F /* HUDViewTableViewCell.swift */,
22302238
A91D2A3E26CF0FF80023B075 /* IconTitleSubtitleTableViewCell.swift */,
@@ -2583,6 +2591,7 @@
25832591
897A5A9724C22DCE00C4E71D /* View Models */ = {
25842592
isa = PBXGroup;
25852593
children = (
2594+
1452F4A82A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift */,
25862595
897A5A9824C22DE800C4E71D /* BolusEntryViewModel.swift */,
25872596
A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */,
25882597
14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */,
@@ -3631,6 +3640,7 @@
36313640
43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */,
36323641
E9BB27AB23B85C3500FB4987 /* SleepStore.swift in Sources */,
36333642
C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */,
3643+
1452F4AD2A851F8800F8B9E4 /* HowAbsorptionTimeWorksView.swift in Sources */,
36343644
C16575712538A36B004AE16E /* CGMStalenessMonitor.swift in Sources */,
36353645
1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */,
36363646
C11BD0552523CFED00236B08 /* SimpleBolusViewModel.swift in Sources */,
@@ -3648,6 +3658,7 @@
36483658
142CB75B2A60BFC30075748A /* FavoriteFoodsView.swift in Sources */,
36493659
A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */,
36503660
4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */,
3661+
1452F4AB2A851EDF00F8B9E4 /* AddEditFavoriteFoodView.swift in Sources */,
36513662
C1742332259BEADC00399C9D /* ManualEntryDoseView.swift in Sources */,
36523663
43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */,
36533664
43FCEEA9221A615B0013DD30 /* StatusChartsManager.swift in Sources */,
@@ -3782,6 +3793,7 @@
37823793
430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */,
37833794
43A51E211EB6DBDD000736CC /* LoopChartsTableViewController.swift in Sources */,
37843795
8968B1122408B3520074BB48 /* UIFont.swift in Sources */,
3796+
1452F4A92A851C9400F8B9E4 /* AddEditFavoriteFoodViewModel.swift in Sources */,
37853797
438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */,
37863798
89A1B66E24ABFDF800117AC2 /* SupportedBolusVolumesUserInfo.swift in Sources */,
37873799
C1C660D1252E4DD5009B5C32 /* LoopConstants.swift in Sources */,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//
2+
// AddEditFavoriteFoodViewModel.swift
3+
// Loop
4+
//
5+
// Created by Noah Brauner on 7/31/23.
6+
// Copyright © 2023 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
import LoopKit
11+
import HealthKit
12+
13+
final class AddEditFavoriteFoodViewModel: ObservableObject {
14+
enum Alert: Identifiable {
15+
var id: Self {
16+
return self
17+
}
18+
19+
case maxQuantityExceded
20+
case warningQuantityValidation
21+
}
22+
23+
@Published var name = ""
24+
25+
@Published var carbsQuantity: Double? = nil
26+
var preferredCarbUnit = HKUnit.gram()
27+
var maxCarbEntryQuantity = LoopConstants.maxCarbEntryQuantity
28+
var warningCarbEntryQuantity = LoopConstants.warningCarbEntryQuantity
29+
30+
@Published var foodType = ""
31+
32+
@Published var absorptionTime: TimeInterval
33+
let minAbsorptionTime = LoopConstants.minCarbAbsorptionTime
34+
let maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime
35+
var absorptionRimesRange: ClosedRange<TimeInterval> {
36+
return minAbsorptionTime...maxAbsorptionTime
37+
}
38+
39+
@Published var alert: AddEditFavoriteFoodViewModel.Alert?
40+
41+
private let onSave: (NewFavoriteFood) -> ()
42+
43+
init(originalFavoriteFood: StoredFavoriteFood?, onSave: @escaping (NewFavoriteFood) -> ()) {
44+
self.onSave = onSave
45+
if let food = originalFavoriteFood {
46+
self.originalFavoriteFood = food
47+
self.name = food.name
48+
self.carbsQuantity = food.carbsQuantity.doubleValue(for: preferredCarbUnit)
49+
self.foodType = food.foodType
50+
self.absorptionTime = food.absorptionTime
51+
}
52+
else {
53+
self.absorptionTime = .hours(3)
54+
}
55+
}
56+
57+
init(carbsQuantity: Double?, foodType: String, absorptionTime: TimeInterval, onSave: @escaping (NewFavoriteFood) -> ()) {
58+
self.onSave = onSave
59+
self.carbsQuantity = carbsQuantity
60+
self.foodType = foodType
61+
self.absorptionTime = absorptionTime
62+
}
63+
64+
var originalFavoriteFood: StoredFavoriteFood?
65+
var updatedFavoriteFood: NewFavoriteFood? {
66+
if let quantity = carbsQuantity, quantity != 0, name != "", foodType != "" {
67+
if let o = originalFavoriteFood, o.name == name, o.carbsQuantity.doubleValue(for: preferredCarbUnit) == carbsQuantity && o.foodType == foodType && o.absorptionTime == absorptionTime {
68+
return nil // No changes were made
69+
}
70+
71+
return NewFavoriteFood(
72+
name: name,
73+
carbsQuantity: HKQuantity(unit: preferredCarbUnit, doubleValue: quantity),
74+
foodType: foodType,
75+
absorptionTime: absorptionTime
76+
)
77+
}
78+
else {
79+
return nil
80+
}
81+
}
82+
83+
func save() {
84+
guard let updatedFavoriteFood, absorptionTime <= maxAbsorptionTime else { return }
85+
86+
guard let carbsQuantity, carbsQuantity > 0 else { return }
87+
let quantity = HKQuantity(unit: preferredCarbUnit, doubleValue: carbsQuantity)
88+
if quantity.compare(maxCarbEntryQuantity) == .orderedDescending {
89+
self.alert = .maxQuantityExceded
90+
return
91+
}
92+
else if quantity.compare(warningCarbEntryQuantity) == .orderedDescending {
93+
self.alert = .warningQuantityValidation
94+
return
95+
}
96+
97+
onSave(updatedFavoriteFood)
98+
}
99+
100+
func clearAlertAndSave() {
101+
guard let updatedFavoriteFood else { return }
102+
self.alert = nil
103+
onSave(updatedFavoriteFood)
104+
}
105+
106+
func clearAlert() {
107+
self.alert = nil
108+
}
109+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// AddEditFavoriteFoodView.swift
3+
// Loop
4+
//
5+
// Created by Noah Brauner on 7/31/23.
6+
// Copyright © 2023 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
import LoopKit
11+
import LoopKitUI
12+
13+
struct AddEditFavoriteFoodView: View {
14+
@Environment(\.dismiss) var dismiss
15+
16+
@StateObject private var viewModel: AddEditFavoriteFoodViewModel
17+
18+
@State private var expandedRow: Row?
19+
@State private var showHowAbsorptionTimeWorks = false
20+
21+
private var isNewEntry = true
22+
23+
/// Initializer for adding a new favorite food or editing a `StoredFavoriteFood`
24+
init(originalFavoriteFood: StoredFavoriteFood? = nil, onSave: @escaping (NewFavoriteFood) -> Void) {
25+
self._viewModel = StateObject(wrappedValue: AddEditFavoriteFoodViewModel(originalFavoriteFood: originalFavoriteFood, onSave: onSave))
26+
self.isNewEntry = originalFavoriteFood == nil
27+
}
28+
29+
/// Initializer for presenting the `AddEditFavoriteFoodView` prepopulated from the `CarbEntryView`
30+
init(carbsQuantity: Double?, foodType: String, absorptionTime: TimeInterval, onSave: @escaping (NewFavoriteFood) -> Void) {
31+
self._viewModel = StateObject(wrappedValue: AddEditFavoriteFoodViewModel(carbsQuantity: carbsQuantity, foodType: foodType, absorptionTime: absorptionTime, onSave: onSave))
32+
}
33+
34+
var body: some View {
35+
if isNewEntry {
36+
NavigationView {
37+
content
38+
.toolbar {
39+
ToolbarItem(placement: .navigationBarLeading) {
40+
dismissButton
41+
}
42+
43+
ToolbarItem(placement: .navigationBarTrailing) {
44+
saveButton
45+
}
46+
}
47+
.navigationBarTitle("New Favorite Food", displayMode: .inline)
48+
.onAppear {
49+
expandedRow = .name
50+
}
51+
}
52+
}
53+
else {
54+
content
55+
.toolbar {
56+
ToolbarItem(placement: .navigationBarLeading) {
57+
if viewModel.updatedFavoriteFood != nil {
58+
dismissButton
59+
}
60+
}
61+
62+
ToolbarItem(placement: .navigationBarTrailing) {
63+
saveButton
64+
}
65+
}
66+
.navigationBarBackButtonHidden(viewModel.updatedFavoriteFood != nil)
67+
.navigationBarTitle(viewModel.originalFavoriteFood?.title ?? "", displayMode: .inline)
68+
}
69+
}
70+
71+
private var content: some View {
72+
ZStack {
73+
Color(.systemGroupedBackground)
74+
.edgesIgnoringSafeArea(.all)
75+
76+
ScrollView {
77+
card
78+
.padding(.top, 8)
79+
80+
saveActionButton
81+
}
82+
}
83+
.alert(item: $viewModel.alert, content: alert(for:))
84+
.sheet(isPresented: $showHowAbsorptionTimeWorks) {
85+
HowAbsorptionTimeWorksView()
86+
}
87+
}
88+
89+
private var card: some View {
90+
VStack(spacing: 10) {
91+
TextFieldRow(text: $viewModel.name, title: "Name", placeholder: "Apple", expandedRow: $expandedRow, row: .name)
92+
93+
CardSectionDivider()
94+
95+
CarbQuantityRow(quantity: $viewModel.carbsQuantity, title: "Carb Quantity", preferredCarbUnit: viewModel.preferredCarbUnit, expandedRow: $expandedRow, row: Row.amountConsumed)
96+
97+
CardSectionDivider()
98+
99+
EmojiRow(emojiType: .food, text: $viewModel.foodType, title: "Food Type", expandedRow: $expandedRow, row: .foodType)
100+
101+
CardSectionDivider()
102+
103+
AbsorptionTimePickerRow(absorptionTime: $viewModel.absorptionTime, validDurationRange: viewModel.absorptionRimesRange, expandedRow: $expandedRow, row: Row.absorptionTime, showHowAbsorptionTimeWorks: $showHowAbsorptionTimeWorks)
104+
.padding(.bottom, 2)
105+
}
106+
.padding(.vertical, 12)
107+
.padding(.horizontal)
108+
.background(CardBackground())
109+
.padding(.horizontal)
110+
}
111+
112+
private func alert(for alert: AddEditFavoriteFoodViewModel.Alert) -> SwiftUI.Alert {
113+
switch alert {
114+
case .maxQuantityExceded:
115+
let message = String(
116+
format: NSLocalizedString("The maximum allowed amount is %@ grams.", comment: "Alert body displayed for quantity greater than max (1: maximum quantity in grams)"),
117+
NumberFormatter.localizedString(from: NSNumber(value: viewModel.maxCarbEntryQuantity.doubleValue(for: viewModel.preferredCarbUnit)), number: .none)
118+
)
119+
let okMessage = NSLocalizedString("com.loudnate.LoopKit.errorAlertActionTitle", value: "OK", comment: "The title of the action used to dismiss an error alert")
120+
return SwiftUI.Alert(
121+
title: Text("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered"),
122+
message: Text(message),
123+
dismissButton: .cancel(Text(okMessage), action: viewModel.clearAlert)
124+
)
125+
case .warningQuantityValidation:
126+
let message = String(
127+
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)"),
128+
NumberFormatter.localizedString(from: NSNumber(value: viewModel.carbsQuantity ?? 0), number: .none)
129+
)
130+
return SwiftUI.Alert(
131+
title: Text("Large Meal Entered", comment: "Title of the warning shown when a large meal was entered"),
132+
message: Text(message),
133+
primaryButton: .default(Text("No, edit amount", comment: "The title of the action used when rejecting the the amount of carbohydrates entered."), action: viewModel.clearAlert),
134+
secondaryButton: .cancel(Text("Yes", comment: "The title of the action used when confirming entered amount of carbohydrates."), action: viewModel.clearAlertAndSave)
135+
)
136+
}
137+
}
138+
}
139+
140+
extension AddEditFavoriteFoodView {
141+
private var dismissButton: some View {
142+
Button(action: dismiss.callAsFunction) {
143+
Text("Cancel")
144+
}
145+
}
146+
147+
private var saveActionButton: some View {
148+
Button(action: viewModel.save) {
149+
Text("Save")
150+
}
151+
.buttonStyle(ActionButtonStyle())
152+
.padding()
153+
.disabled(viewModel.updatedFavoriteFood == nil)
154+
}
155+
156+
private var saveButton: some View {
157+
Button(action: viewModel.save) {
158+
Text("Save")
159+
}
160+
.disabled(viewModel.updatedFavoriteFood == nil)
161+
}
162+
}
163+
164+
extension AddEditFavoriteFoodView {
165+
enum Row {
166+
case name, amountConsumed, foodType, absorptionTime
167+
}
168+
}

Loop/Views/FavoriteFoodsView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ struct FavoriteFoodsView: View {
4848
.insetGroupedListStyle()
4949

5050

51-
NavigationLink(destination: Text("Edit View"), isActive: $viewModel.isEditViewActive) {
51+
NavigationLink(destination: AddEditFavoriteFoodView(originalFavoriteFood: viewModel.selectedFood, onSave: viewModel.onFoodSave(_:)), isActive: $viewModel.isEditViewActive) {
5252
EmptyView()
5353
}
5454

55-
NavigationLink(destination: Text("Detail View"), isActive: $viewModel.isDetailViewActive) {
55+
NavigationLink(destination: FavoriteFoodDetailView(food: viewModel.selectedFood, onFoodDelete: viewModel.onFoodDelete(_:)), isActive: $viewModel.isDetailViewActive) {
5656
EmptyView()
5757
}
5858
}
@@ -64,7 +64,7 @@ struct FavoriteFoodsView: View {
6464
.navigationBarTitle("Favorite Foods", displayMode: .large)
6565
}
6666
.sheet(isPresented: $viewModel.isAddViewActive) {
67-
Text("Add View")
67+
AddEditFavoriteFoodView(onSave: viewModel.onFoodSave(_:))
6868
}
6969
.onChange(of: editMode) { newValue in
7070
if !newValue.isEditing {

0 commit comments

Comments
 (0)