RemoteSettingsView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. @FocusState private var focusedField: Field?
  15. @State private var showAlert: Bool = false
  16. @State private var alertType: AlertType? = nil
  17. @State private var alertMessage: String? = nil
  18. enum Field: Hashable {
  19. case user
  20. case deviceToken
  21. case sharedSecret
  22. case apnsKey
  23. case teamId
  24. case keyId
  25. case bundleId
  26. case maxBolus
  27. }
  28. enum AlertType {
  29. case validation
  30. }
  31. var body: some View {
  32. NavigationView {
  33. Form {
  34. // Remote Type Section
  35. Section {
  36. Picker("Remote Type", selection: $viewModel.remoteType) {
  37. Text("None").tag(RemoteType.none)
  38. Text("Nightscout").tag(RemoteType.nightscout)
  39. Text("Trio Remote Control").tag(RemoteType.trc)
  40. }
  41. .pickerStyle(MenuPickerStyle())
  42. }
  43. // User Information Section
  44. if viewModel.remoteType != .none {
  45. Section(header: Text("User Information")) {
  46. HStack {
  47. Text("User")
  48. TextField("Enter User", text: $viewModel.user)
  49. .autocapitalization(.none)
  50. .disableAutocorrection(true)
  51. .focused($focusedField, equals: .user)
  52. .multilineTextAlignment(.trailing)
  53. }
  54. }
  55. }
  56. // Trio Remote Control Settings Section
  57. if viewModel.remoteType == .trc {
  58. Section(header: Text("Trio Remote Control Settings")) {
  59. HStack {
  60. Text("Device Token")
  61. TextField("Enter Device Token", text: $viewModel.deviceToken)
  62. .autocapitalization(.none)
  63. .disableAutocorrection(true)
  64. .focused($focusedField, equals: .deviceToken)
  65. .multilineTextAlignment(.trailing)
  66. .disabled(true)
  67. }
  68. HStack {
  69. Text("Shared Secret")
  70. TextField("Enter Shared Secret", text: $viewModel.sharedSecret)
  71. .autocapitalization(.none)
  72. .disableAutocorrection(true)
  73. .focused($focusedField, equals: .sharedSecret)
  74. .multilineTextAlignment(.trailing)
  75. }
  76. Toggle("Production Environment", isOn: $viewModel.productionEnvironment)
  77. .padding(.vertical, 5)
  78. .disabled(true)
  79. HStack {
  80. Text("APNS Key ID")
  81. TextField("Enter APNS Key ID", text: $viewModel.keyId)
  82. .autocapitalization(.none)
  83. .disableAutocorrection(true)
  84. .focused($focusedField, equals: .keyId)
  85. .multilineTextAlignment(.trailing)
  86. }
  87. VStack(alignment: .leading) {
  88. Text("APNS Key")
  89. TextEditor(text: $viewModel.apnsKey)
  90. .frame(height: 100)
  91. .autocapitalization(.none)
  92. .disableAutocorrection(true)
  93. .overlay(
  94. RoundedRectangle(cornerRadius: 8)
  95. .stroke(Color.gray.opacity(0.5), lineWidth: 1)
  96. )
  97. .focused($focusedField, equals: .apnsKey)
  98. }
  99. HStack {
  100. Text("Team ID")
  101. TextField("Enter Team ID", text: $viewModel.teamId)
  102. .autocapitalization(.none)
  103. .disableAutocorrection(true)
  104. .focused($focusedField, equals: .teamId)
  105. .multilineTextAlignment(.trailing)
  106. }
  107. HStack {
  108. Text("Bundle ID")
  109. TextField("Enter Bundle ID", text: $viewModel.bundleId)
  110. .autocapitalization(.none)
  111. .disableAutocorrection(true)
  112. .focused($focusedField, equals: .bundleId)
  113. .multilineTextAlignment(.trailing)
  114. }
  115. .disabled(true)
  116. }
  117. // Guardrails Section
  118. Section(header: Text("Guardrails")) {
  119. HStack {
  120. Text("Max Bolus")
  121. Spacer()
  122. TextFieldWithToolBar(
  123. quantity: $viewModel.maxBolus,
  124. maxLength: 4,
  125. unit: HKUnit.internationalUnit(),
  126. allowDecimalSeparator: true,
  127. minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.0),
  128. maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 10.0),
  129. onValidationError: { message in
  130. handleValidationError(message)
  131. }
  132. )
  133. .frame(width: 100)
  134. Text("U")
  135. .foregroundColor(.secondary)
  136. }
  137. HStack {
  138. Text("Max Carbs")
  139. Spacer()
  140. TextFieldWithToolBar(
  141. quantity: $viewModel.maxCarbs,
  142. maxLength: 4,
  143. unit: HKUnit.gram(),
  144. allowDecimalSeparator: true,
  145. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  146. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  147. onValidationError: { message in
  148. handleValidationError(message)
  149. }
  150. )
  151. .frame(width: 100)
  152. Text("g")
  153. .foregroundColor(.secondary)
  154. }
  155. HStack {
  156. Text("Max Protein")
  157. Spacer()
  158. TextFieldWithToolBar(
  159. quantity: $viewModel.maxProtein,
  160. maxLength: 4,
  161. unit: HKUnit.gram(),
  162. allowDecimalSeparator: true,
  163. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  164. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  165. onValidationError: { message in
  166. handleValidationError(message)
  167. }
  168. )
  169. .frame(width: 100)
  170. Text("g")
  171. .foregroundColor(.secondary)
  172. }
  173. HStack {
  174. Text("Max Fat")
  175. Spacer()
  176. TextFieldWithToolBar(
  177. quantity: $viewModel.maxFat,
  178. maxLength: 4,
  179. unit: HKUnit.gram(),
  180. allowDecimalSeparator: true,
  181. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  182. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  183. onValidationError: { message in
  184. handleValidationError(message)
  185. }
  186. )
  187. .frame(width: 100)
  188. Text("g")
  189. .foregroundColor(.secondary)
  190. }
  191. }
  192. }
  193. }
  194. .navigationBarTitle("Remote Settings", displayMode: .inline)
  195. .toolbar {
  196. ToolbarItem(placement: .navigationBarTrailing) {
  197. Button("Done") {
  198. presentationMode.wrappedValue.dismiss()
  199. }
  200. }
  201. }
  202. .onTapGesture {
  203. focusedField = nil
  204. }
  205. .alert(isPresented: $showAlert) {
  206. switch alertType {
  207. case .validation:
  208. return Alert(
  209. title: Text("Validation Error"),
  210. message: Text(alertMessage ?? "Invalid input."),
  211. dismissButton: .default(Text("OK"))
  212. )
  213. case .none:
  214. return Alert(title: Text("Unknown Alert"))
  215. }
  216. }
  217. }
  218. }
  219. private func handleValidationError(_ message: String) {
  220. alertMessage = message
  221. alertType = .validation
  222. showAlert = true
  223. }
  224. }