RemoteSettingsView.swift 9.7 KB

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