RemoteSettingsView.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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("Shared Secret")
  61. TextField("Enter Shared Secret", text: $viewModel.sharedSecret)
  62. .autocapitalization(.none)
  63. .disableAutocorrection(true)
  64. .focused($focusedField, equals: .sharedSecret)
  65. .multilineTextAlignment(.trailing)
  66. }
  67. HStack {
  68. Text("APNS Key ID")
  69. TextField("Enter APNS Key ID", text: $viewModel.keyId)
  70. .autocapitalization(.none)
  71. .disableAutocorrection(true)
  72. .focused($focusedField, equals: .keyId)
  73. .multilineTextAlignment(.trailing)
  74. }
  75. VStack(alignment: .leading) {
  76. Text("APNS Key")
  77. TextEditor(text: $viewModel.apnsKey)
  78. .frame(height: 100)
  79. .autocapitalization(.none)
  80. .disableAutocorrection(true)
  81. .overlay(
  82. RoundedRectangle(cornerRadius: 8)
  83. .stroke(Color.gray.opacity(0.5), lineWidth: 1)
  84. )
  85. .focused($focusedField, equals: .apnsKey)
  86. }
  87. HStack {
  88. Text("Team ID")
  89. TextField("Enter Team ID", text: $viewModel.teamId)
  90. .autocapitalization(.none)
  91. .disableAutocorrection(true)
  92. .focused($focusedField, equals: .teamId)
  93. .multilineTextAlignment(.trailing)
  94. }
  95. }
  96. // Guardrails Section
  97. Section(header: Text("Guardrails")) {
  98. HStack {
  99. Text("Max Bolus")
  100. Spacer()
  101. TextFieldWithToolBar(
  102. quantity: $viewModel.maxBolus,
  103. maxLength: 4,
  104. unit: HKUnit.internationalUnit(),
  105. allowDecimalSeparator: true,
  106. minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.0),
  107. maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 10.0),
  108. onValidationError: { message in
  109. handleValidationError(message)
  110. }
  111. )
  112. .frame(width: 100)
  113. Text("U")
  114. .foregroundColor(.secondary)
  115. }
  116. HStack {
  117. Text("Max Carbs")
  118. Spacer()
  119. TextFieldWithToolBar(
  120. quantity: $viewModel.maxCarbs,
  121. maxLength: 4,
  122. unit: HKUnit.gram(),
  123. allowDecimalSeparator: true,
  124. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  125. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  126. onValidationError: { message in
  127. handleValidationError(message)
  128. }
  129. )
  130. .frame(width: 100)
  131. Text("g")
  132. .foregroundColor(.secondary)
  133. }
  134. HStack {
  135. Text("Max Protein")
  136. Spacer()
  137. TextFieldWithToolBar(
  138. quantity: $viewModel.maxProtein,
  139. maxLength: 4,
  140. unit: HKUnit.gram(),
  141. allowDecimalSeparator: true,
  142. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  143. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  144. onValidationError: { message in
  145. handleValidationError(message)
  146. }
  147. )
  148. .frame(width: 100)
  149. Text("g")
  150. .foregroundColor(.secondary)
  151. }
  152. HStack {
  153. Text("Max Fat")
  154. Spacer()
  155. TextFieldWithToolBar(
  156. quantity: $viewModel.maxFat,
  157. maxLength: 4,
  158. unit: HKUnit.gram(),
  159. allowDecimalSeparator: true,
  160. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  161. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  162. onValidationError: { message in
  163. handleValidationError(message)
  164. }
  165. )
  166. .frame(width: 100)
  167. Text("g")
  168. .foregroundColor(.secondary)
  169. }
  170. }
  171. }
  172. }
  173. .navigationBarTitle("Remote Settings", displayMode: .inline)
  174. .toolbar {
  175. ToolbarItem(placement: .navigationBarTrailing) {
  176. Button("Done") {
  177. presentationMode.wrappedValue.dismiss()
  178. }
  179. }
  180. }
  181. .onTapGesture {
  182. focusedField = nil
  183. }
  184. .alert(isPresented: $showAlert) {
  185. switch alertType {
  186. case .validation:
  187. return Alert(
  188. title: Text("Validation Error"),
  189. message: Text(alertMessage ?? "Invalid input."),
  190. dismissButton: .default(Text("OK"))
  191. )
  192. case .none:
  193. return Alert(title: Text("Unknown Alert"))
  194. }
  195. }
  196. }
  197. }
  198. private func handleValidationError(_ message: String) {
  199. alertMessage = message
  200. alertType = .validation
  201. showAlert = true
  202. }
  203. }