SettingsViewController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. //
  2. // SettingsViewController.swift
  3. // LoopFollow
  4. //
  5. // Created by Jon Fawcett on 6/3/20.
  6. // Copyright © 2020 Jon Fawcett. All rights reserved.
  7. //
  8. import UIKit
  9. import Eureka
  10. import EventKit
  11. import EventKitUI
  12. class SettingsViewController: FormViewController {
  13. var appStateController: AppStateController?
  14. var statusLabelRow: LabelRow!
  15. func showHideNSDetails() {
  16. var isHidden = false
  17. var isEnabled = true
  18. var isLoopHidden = false;
  19. if UserDefaultsRepository.url.value == "" || !UserDefaultsRepository.loopUser.value {
  20. isHidden = true
  21. isEnabled = false
  22. }
  23. if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow {
  24. row1.hidden = .function(["hide"], {form in
  25. return isHidden
  26. })
  27. row1.evaluateHidden()
  28. }
  29. if UserDefaultsRepository.url.value != "" {
  30. isEnabled = true
  31. }
  32. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  33. nightscoutTab.isEnabled = isEnabled
  34. }
  35. // Determine if the build is from TestFlight
  36. func isTestFlightBuild() -> Bool {
  37. #if targetEnvironment(simulator)
  38. return false
  39. #else
  40. if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil {
  41. return false
  42. }
  43. guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else {
  44. return false
  45. }
  46. return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame
  47. #endif
  48. }
  49. // Get the build date from the build details
  50. func buildDate() -> Date? {
  51. let dateFormatter = DateFormatter()
  52. dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy"
  53. dateFormatter.locale = Locale(identifier: "en_US_POSIX")
  54. dateFormatter.timeZone = TimeZone(identifier: "UTC")
  55. guard let dateString = BuildDetails.default.buildDateString,
  56. let date = dateFormatter.date(from: dateString) else {
  57. return nil
  58. }
  59. return date
  60. }
  61. // Calculate the expiration date based on the build type
  62. func calculateExpirationDate() -> Date {
  63. if isTestFlightBuild(), let buildDate = buildDate() {
  64. // For TestFlight, add 90 days to the build date
  65. return Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
  66. } else {
  67. // For Xcode builds, use the provisioning profile's expiration date
  68. if let provision = MobileProvision.read() {
  69. return provision.expirationDate
  70. } else {
  71. return Date() // Fallback to current date if unable to read provisioning profile
  72. }
  73. }
  74. }
  75. override func viewDidLoad() {
  76. super.viewDidLoad()
  77. if UserDefaultsRepository.forceDarkMode.value {
  78. overrideUserInterfaceStyle = .dark
  79. }
  80. UserDefaultsRepository.showNS.value = false
  81. UserDefaultsRepository.showDex.value = false
  82. let expiration = calculateExpirationDate()
  83. form
  84. +++ Section(header: "Data Settings", footer: "")
  85. <<< SegmentedRow<String>("units") { row in
  86. row.title = "Units"
  87. row.options = ["mg/dL", "mmol/L"]
  88. row.value = UserDefaultsRepository.units.value
  89. }.onChange { row in
  90. guard let value = row.value else { return }
  91. UserDefaultsRepository.units.value = value
  92. }
  93. <<< SwitchRow("showNS"){ row in
  94. row.title = "Show Nightscout Settings"
  95. row.value = UserDefaultsRepository.showNS.value
  96. }.onChange { [weak self] row in
  97. guard let value = row.value else { return }
  98. UserDefaultsRepository.showNS.value = value
  99. }
  100. <<< TextRow() { row in
  101. row.title = "URL"
  102. row.placeholder = "https://mycgm.herokuapp.com"
  103. row.value = UserDefaultsRepository.url.value
  104. row.hidden = "$showNS == false"
  105. }.cellSetup { (cell, row) in
  106. cell.textField.autocorrectionType = .no
  107. cell.textField.autocapitalizationType = .none
  108. }.onChange { row in
  109. guard let value = row.value else {
  110. UserDefaultsRepository.url.value = ""
  111. self.showHideNSDetails()
  112. return }
  113. // check the format of the URL entered by the user and trim away any spaces or "/" at the end
  114. var urlNSInput = value.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
  115. if urlNSInput.last == "/" {
  116. urlNSInput = String(urlNSInput.dropLast())
  117. }
  118. UserDefaultsRepository.url.value = urlNSInput.lowercased()
  119. // set the row value back to the correctly formatted URL so that the user immediately sees how it should have been written
  120. row.value = UserDefaultsRepository.url.value
  121. self.showHideNSDetails()
  122. globalVariables.nsVerifiedAlert = 0
  123. // Verify Nightscout URL and token
  124. self.checkNightscoutStatus()
  125. }
  126. <<< TextRow() { row in
  127. row.title = "NS Token"
  128. row.placeholder = "Leave blank if not using tokens"
  129. row.value = UserDefaultsRepository.token.value
  130. row.hidden = "$showNS == false"
  131. }.cellSetup { (cell, row) in
  132. cell.textField.autocorrectionType = .no
  133. cell.textField.autocapitalizationType = .none
  134. cell.textField.textContentType = .password
  135. }.onChange { row in
  136. if row.value == nil {
  137. UserDefaultsRepository.token.value = ""
  138. }
  139. guard let value = row.value else { return }
  140. UserDefaultsRepository.token.value = value
  141. globalVariables.nsVerifiedAlert = 0
  142. // Verify Nightscout URL and token
  143. self.checkNightscoutStatus()
  144. }
  145. <<< LabelRow() { row in
  146. row.title = "NS Status"
  147. row.value = "Checking..."
  148. statusLabelRow = row
  149. row.hidden = "$showNS == false"
  150. }
  151. <<< SwitchRow("loopUser"){ row in
  152. row.title = "Download Loop/iAPS Data"
  153. row.tag = "loopUser"
  154. row.value = UserDefaultsRepository.loopUser.value
  155. row.hidden = "$showNS == false"
  156. }.onChange { [weak self] row in
  157. guard let value = row.value else { return }
  158. UserDefaultsRepository.loopUser.value = value
  159. }
  160. <<< SwitchRow("showDex"){ row in
  161. row.title = "Show Dexcom Settings"
  162. row.value = UserDefaultsRepository.showDex.value
  163. }.onChange { [weak self] row in
  164. guard let value = row.value else { return }
  165. UserDefaultsRepository.showDex.value = value
  166. }
  167. <<< TextRow(){ row in
  168. row.title = "User Name"
  169. row.value = UserDefaultsRepository.shareUserName.value
  170. row.hidden = "$showDex == false"
  171. }.cellSetup { (cell, row) in
  172. cell.textField.autocorrectionType = .no
  173. cell.textField.autocapitalizationType = .none
  174. }.onChange { row in
  175. if row.value == nil {
  176. UserDefaultsRepository.shareUserName.value = ""
  177. }
  178. guard let value = row.value else { return }
  179. UserDefaultsRepository.shareUserName.value = value
  180. globalVariables.dexVerifiedAlert = 0
  181. }
  182. <<< TextRow(){ row in
  183. row.title = "Password"
  184. row.value = UserDefaultsRepository.sharePassword.value
  185. row.hidden = "$showDex == false"
  186. }.cellSetup { (cell, row) in
  187. cell.textField.autocorrectionType = .no
  188. cell.textField.isSecureTextEntry = true
  189. cell.textField.autocapitalizationType = .none
  190. }.onChange { row in
  191. if row.value == nil {
  192. UserDefaultsRepository.sharePassword.value = ""
  193. }
  194. guard let value = row.value else { return }
  195. UserDefaultsRepository.sharePassword.value = value
  196. globalVariables.dexVerifiedAlert = 0
  197. }
  198. <<< SegmentedRow<String>("shareServer") { row in
  199. row.title = "Server"
  200. row.options = ["US", "NON-US"]
  201. row.value = UserDefaultsRepository.shareServer.value
  202. row.hidden = "$showDex == false"
  203. }.onChange { row in
  204. guard let value = row.value else { return }
  205. UserDefaultsRepository.shareServer.value = value
  206. }
  207. +++ Section("App Settings")
  208. <<< ButtonRow() {
  209. $0.title = "General Settings"
  210. $0.presentationMode = .show(
  211. controllerProvider: .callback(builder: {
  212. let controller = GeneralSettingsViewController()
  213. controller.appStateController = self.appStateController
  214. return controller
  215. }
  216. ), onDismiss: nil)
  217. }
  218. <<< ButtonRow("graphSettings") {
  219. $0.title = "Graph Settings"
  220. $0.presentationMode = .show(
  221. controllerProvider: .callback(builder: {
  222. let controller = GraphSettingsViewController()
  223. controller.appStateController = self.appStateController
  224. return controller
  225. }
  226. ), onDismiss: nil)
  227. }
  228. <<< ButtonRow("informationDisplaySettings") {
  229. $0.title = "Information Display Settings"
  230. $0.presentationMode = .show(
  231. controllerProvider: .callback(builder: {
  232. let controller = InfoDisplaySettingsViewController()
  233. controller.appStateController = self.appStateController
  234. return controller
  235. }
  236. ), onDismiss: nil)
  237. }
  238. +++ Section("Integrations")
  239. <<< ButtonRow() {
  240. $0.title = "Apple Watch and Carplay"
  241. $0.presentationMode = .show(
  242. controllerProvider: .callback(builder: {
  243. let controller = WatchSettingsViewController()
  244. controller.appStateController = self.appStateController
  245. return controller
  246. }
  247. ), onDismiss: nil)
  248. }
  249. <<< LabelRow("Clear Images"){ row in
  250. row.title = "Delete Watch Face Images"
  251. }.onCellSelection{ cell,row in
  252. if UserDefaultsRepository.saveImage.value {
  253. guard let mainScreen = self.tabBarController!.viewControllers?[0] as? MainViewController else { return }
  254. mainScreen.deleteOldImages()
  255. mainScreen.saveChartImage()
  256. }
  257. }
  258. +++ Section("Advanced Settings")
  259. <<< ButtonRow() {
  260. $0.title = "Advanced Settings"
  261. $0.presentationMode = .show(
  262. controllerProvider: .callback(builder: {
  263. let controller = AdvancedSettingsViewController()
  264. controller.appStateController = self.appStateController
  265. return controller
  266. }
  267. ), onDismiss: nil)
  268. }
  269. +++ Section(header: getAppVersion(), footer: "")
  270. +++ Section(header: "App Expiration", footer: String(expiration.description))
  271. showHideNSDetails()
  272. checkNightscoutStatus()
  273. }
  274. func getAppVersion() -> String {
  275. if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
  276. return "App Version: \(version)"
  277. }
  278. return "Version Unknown"
  279. }
  280. func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
  281. if let error = error {
  282. switch error {
  283. case .invalidURL:
  284. statusLabelRow.value = "Invalid URL"
  285. case .networkError:
  286. statusLabelRow.value = "Network Error"
  287. case .invalidToken:
  288. statusLabelRow.value = "Invalid Token"
  289. case .tokenRequired:
  290. statusLabelRow.value = "Token Required"
  291. case .siteNotFound:
  292. statusLabelRow.value = "Site Not Found"
  293. case .unknown:
  294. statusLabelRow.value = "Unknown Error"
  295. case .emptyAddress:
  296. statusLabelRow.value = "Address Empty"
  297. }
  298. } else {
  299. statusLabelRow.value = "OK"
  300. NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
  301. }
  302. statusLabelRow.updateCell()
  303. }
  304. func checkNightscoutStatus() {
  305. NightscoutUtils.verifyURLAndToken(urlUser: UserDefaultsRepository.url.value, token: UserDefaultsRepository.token.value) { error in
  306. DispatchQueue.main.async {
  307. self.updateStatusLabel(error: error)
  308. }
  309. }
  310. }
  311. }