RemoteSettingsView.swift 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. //
  2. // RemoteSettingsView.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2024-08-25.
  6. // Updated on 2024-09-16.
  7. // Copyright © 2024 Jon Fawcett. All rights reserved.
  8. //
  9. import SwiftUI
  10. import HealthKit
  11. struct RemoteSettingsView: View {
  12. @ObservedObject var viewModel: RemoteSettingsViewModel
  13. @Environment(\.presentationMode) var presentationMode
  14. @State private var showAlert: Bool = false
  15. @State private var alertType: AlertType? = nil
  16. @State private var alertMessage: String? = nil
  17. enum AlertType {
  18. case validation
  19. }
  20. var body: some View {
  21. NavigationView {
  22. Form {
  23. // MARK: - Remote Type Section (Custom Rows)
  24. Section(header: Text("Remote Type")) {
  25. remoteTypeRow(type: .none, label: "None", isEnabled: true)
  26. remoteTypeRow(type: .nightscout, label: "Nightscout", isEnabled: true)
  27. remoteTypeRow(
  28. type: .trc,
  29. label: "Trio Remote Control",
  30. isEnabled: viewModel.isTrioDevice
  31. )
  32. Text("Nightscout is the only option for the released version of Trio and Loop.")
  33. .font(.footnote)
  34. .foregroundColor(.secondary)
  35. }
  36. // MARK: - User Information Section
  37. if viewModel.remoteType != .none {
  38. Section(header: Text("User Information")) {
  39. HStack {
  40. Text("User")
  41. TextField("Enter User", text: $viewModel.user)
  42. .autocapitalization(.none)
  43. .disableAutocorrection(true)
  44. .multilineTextAlignment(.trailing)
  45. }
  46. }
  47. }
  48. // MARK: - Trio Remote Control Settings
  49. if viewModel.remoteType == .trc {
  50. Section(header: Text("Trio Remote Control Settings")) {
  51. HStack {
  52. Text("Shared Secret")
  53. TextField("Enter Shared Secret", text: $viewModel.sharedSecret)
  54. .autocapitalization(.none)
  55. .disableAutocorrection(true)
  56. .multilineTextAlignment(.trailing)
  57. }
  58. HStack {
  59. Text("APNS Key ID")
  60. TextField("Enter APNS Key ID", text: $viewModel.keyId)
  61. .autocapitalization(.none)
  62. .disableAutocorrection(true)
  63. .multilineTextAlignment(.trailing)
  64. }
  65. VStack(alignment: .leading) {
  66. Text("APNS Key")
  67. TextEditor(text: $viewModel.apnsKey)
  68. .frame(height: 100)
  69. .autocapitalization(.none)
  70. .disableAutocorrection(true)
  71. .overlay(
  72. RoundedRectangle(cornerRadius: 8)
  73. .stroke(Color.gray.opacity(0.5), lineWidth: 1)
  74. )
  75. }
  76. }
  77. // MARK: - Guardrails
  78. Section(header: Text("Guardrails")) {
  79. HStack {
  80. Text("Max Bolus")
  81. Spacer()
  82. TextFieldWithToolBar(
  83. quantity: $viewModel.maxBolus,
  84. maxLength: 4,
  85. unit: HKUnit.internationalUnit(),
  86. allowDecimalSeparator: true,
  87. minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.0),
  88. maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 10.0),
  89. onValidationError: { message in
  90. handleValidationError(message)
  91. }
  92. )
  93. .frame(width: 100)
  94. Text("U")
  95. .foregroundColor(.secondary)
  96. }
  97. HStack {
  98. Text("Max Carbs")
  99. Spacer()
  100. TextFieldWithToolBar(
  101. quantity: $viewModel.maxCarbs,
  102. maxLength: 4,
  103. unit: HKUnit.gram(),
  104. allowDecimalSeparator: true,
  105. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  106. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  107. onValidationError: { message in
  108. handleValidationError(message)
  109. }
  110. )
  111. .frame(width: 100)
  112. Text("g")
  113. .foregroundColor(.secondary)
  114. }
  115. HStack {
  116. Text("Max Protein")
  117. Spacer()
  118. TextFieldWithToolBar(
  119. quantity: $viewModel.maxProtein,
  120. maxLength: 4,
  121. unit: HKUnit.gram(),
  122. allowDecimalSeparator: true,
  123. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  124. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  125. onValidationError: { message in
  126. handleValidationError(message)
  127. }
  128. )
  129. .frame(width: 100)
  130. Text("g")
  131. .foregroundColor(.secondary)
  132. }
  133. HStack {
  134. Text("Max Fat")
  135. Spacer()
  136. TextFieldWithToolBar(
  137. quantity: $viewModel.maxFat,
  138. maxLength: 4,
  139. unit: HKUnit.gram(),
  140. allowDecimalSeparator: true,
  141. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  142. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  143. onValidationError: { message in
  144. handleValidationError(message)
  145. }
  146. )
  147. .frame(width: 100)
  148. Text("g")
  149. .foregroundColor(.secondary)
  150. }
  151. }
  152. // MARK: - Meal Section
  153. Section(header: Text("Meal Settings")) {
  154. Toggle("Meal with Bolus", isOn: $viewModel.mealWithBolus)
  155. .toggleStyle(SwitchToggleStyle())
  156. Toggle("Meal with Fat/Protein", isOn: $viewModel.mealWithFatProtein)
  157. .toggleStyle(SwitchToggleStyle())
  158. }
  159. // MARK: - Debug / Info
  160. Section(header: Text("Debug / Info")) {
  161. Text("Device Token: \(Storage.shared.deviceToken.value)")
  162. Text("Production Env.: \(Storage.shared.productionEnvironment.value ? "True" : "False")")
  163. Text("Team ID: \(Storage.shared.teamId.value ?? "")")
  164. Text("Bundle ID: \(Storage.shared.bundleId.value)")
  165. }
  166. }
  167. }
  168. .navigationBarTitle("Remote Settings", displayMode: .inline)
  169. .toolbar {
  170. ToolbarItem(placement: .navigationBarTrailing) {
  171. Button("Done") {
  172. presentationMode.wrappedValue.dismiss()
  173. }
  174. }
  175. }
  176. .alert(isPresented: $showAlert) {
  177. switch alertType {
  178. case .validation:
  179. return Alert(
  180. title: Text("Validation Error"),
  181. message: Text(alertMessage ?? "Invalid input."),
  182. dismissButton: .default(Text("OK"))
  183. )
  184. case .none:
  185. return Alert(title: Text("Unknown Alert"))
  186. }
  187. }
  188. }
  189. }
  190. // MARK: - Custom Row for Remote Type Selection
  191. private func remoteTypeRow(type: RemoteType, label: String, isEnabled: Bool) -> some View {
  192. Button(action: {
  193. if isEnabled {
  194. viewModel.remoteType = type
  195. }
  196. }) {
  197. HStack {
  198. Text(label)
  199. Spacer()
  200. if viewModel.remoteType == type {
  201. Image(systemName: "checkmark")
  202. .foregroundColor(.accentColor)
  203. }
  204. }
  205. }
  206. // If isEnabled is false, user can see the row but not tap it.
  207. .disabled(!isEnabled)
  208. .foregroundColor(isEnabled ? .primary : .gray)
  209. }
  210. // MARK: - Validation Error Handler
  211. private func handleValidationError(_ message: String) {
  212. alertMessage = message
  213. alertType = .validation
  214. showAlert = true
  215. }
  216. }