Browse Source

Basal Graph! Still some work to do, but it's usable. I had to do a lot of code cleanup and reorganizing to get this started

Jon Fawcett 6 năm trước cách đây
mục cha
commit
a537b26e6e
100 tập tin đã thay đổi với 1396 bổ sung1497 xóa
  1. 50 35
      LoopFollow.xcodeproj/project.pbxproj
  2. 0 0
      LoopFollow/Application/AppDelegate.swift
  3. 0 0
      LoopFollow/Application/Base.lproj/LaunchScreen.storyboard
  4. 13 2
      LoopFollow/Base.lproj/Main.storyboard
  5. 0 0
      LoopFollow/Application/SceneDelegate.swift
  6. 0 0
      LoopFollow/Controllers/AlarmSound.swift
  7. 322 0
      LoopFollow/Controllers/Alarms.swift
  8. 239 0
      LoopFollow/Controllers/Graphs.swift
  9. 0 1456
      LoopFollow/Controllers/MainViewController.swift
  10. 771 0
      LoopFollow/Controllers/NightScout.swift
  11. 1 4
      LoopFollow/LoopFollow.xcdatamodeld/.xccurrentversion
  12. 0 0
      LoopFollow/Resources/Alarm Sounds/1-millisecond-of-silence.mp3
  13. 0 0
      LoopFollow/Resources/Alarm Sounds/20ms-of-silence.caf
  14. 0 0
      LoopFollow/Resources/Alarm Sounds/500ms-of-silence.mp3
  15. 0 0
      LoopFollow/Resources/Alarm Sounds/Alarm_Buzzer.caf
  16. 0 0
      LoopFollow/Resources/Alarm Sounds/Alarm_Clock.caf
  17. 0 0
      LoopFollow/Resources/Alarm Sounds/Alert_Tone_Busy.caf
  18. 0 0
      LoopFollow/Resources/Alarm Sounds/Alert_Tone_Ringtone_1.caf
  19. 0 0
      LoopFollow/Resources/Alarm Sounds/Alert_Tone_Ringtone_2.caf
  20. 0 0
      LoopFollow/Resources/Alarm Sounds/Alien_Siren.caf
  21. 0 0
      LoopFollow/Resources/Alarm Sounds/Ambulance.caf
  22. 0 0
      LoopFollow/Resources/Alarm Sounds/Analog_Watch_Alarm.caf
  23. 0 0
      LoopFollow/Resources/Alarm Sounds/Big_Clock_Ticking.caf
  24. 0 0
      LoopFollow/Resources/Alarm Sounds/Burglar_Alarm_Siren_1.caf
  25. 0 0
      LoopFollow/Resources/Alarm Sounds/Burglar_Alarm_Siren_2.caf
  26. 0 0
      LoopFollow/Resources/Alarm Sounds/CSFX-2_Alarm.caf
  27. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Ascend_Climb_Sneaky.caf
  28. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Ascend_Then_Descend.caf
  29. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Bounce_To_Ceiling.caf
  30. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Dreamy_Glissando_Harp.caf
  31. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Fail_Strings_Trumpet.caf
  32. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Machine_Clumsy_Loop.caf
  33. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Siren.caf
  34. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Tip_Toe_Sneaky_Walk.caf
  35. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Uh_Oh.caf
  36. 0 0
      LoopFollow/Resources/Alarm Sounds/Cartoon_Villain_Horns.caf
  37. 0 0
      LoopFollow/Resources/Alarm Sounds/Cell_Phone_Ring_Tone.caf
  38. 0 0
      LoopFollow/Resources/Alarm Sounds/Chimes_Glassy.caf
  39. 0 0
      LoopFollow/Resources/Alarm Sounds/Computer_Magic.caf
  40. 0 0
      LoopFollow/Resources/Alarm Sounds/Cuckoo_Clock.caf
  41. 0 0
      LoopFollow/Resources/Alarm Sounds/Dhol_Shuffleloop.caf
  42. 0 0
      LoopFollow/Resources/Alarm Sounds/Discreet.caf
  43. 0 0
      LoopFollow/Resources/Alarm Sounds/Early_Sunrise.caf
  44. 0 0
      LoopFollow/Resources/Alarm Sounds/Emergency_Alarm.caf
  45. 0 0
      LoopFollow/Resources/Alarm Sounds/Emergency_Alarm_Carbon_Monoxide.caf
  46. 0 0
      LoopFollow/Resources/Alarm Sounds/Emergency_Alarm_Siren.caf
  47. 0 0
      LoopFollow/Resources/Alarm Sounds/Ending_Reached.caf
  48. 0 0
      LoopFollow/Resources/Alarm Sounds/Fly.caf
  49. 0 0
      LoopFollow/Resources/Alarm Sounds/Ghost_Hover.caf
  50. 0 0
      LoopFollow/Resources/Alarm Sounds/Good_Morning.caf
  51. 0 0
      LoopFollow/Resources/Alarm Sounds/Hell_Yeah_Somewhat_Calmer.caf
  52. 0 0
      LoopFollow/Resources/Alarm Sounds/In_A_Hurry.caf
  53. 0 0
      LoopFollow/Resources/Alarm Sounds/Indeed.caf
  54. 0 0
      LoopFollow/Resources/Alarm Sounds/Insistently.caf
  55. 0 0
      LoopFollow/Resources/Alarm Sounds/Jingle_All_The_Way.caf
  56. 0 0
      LoopFollow/Resources/Alarm Sounds/Laser_Shoot.caf
  57. 0 0
      LoopFollow/Resources/Alarm Sounds/Machine_Charge.caf
  58. 0 0
      LoopFollow/Resources/Alarm Sounds/Magical_Twinkle.caf
  59. 0 0
      LoopFollow/Resources/Alarm Sounds/Marching_Heavy_Footed_Fat_Elephants.caf
  60. 0 0
      LoopFollow/Resources/Alarm Sounds/Marimba_Descend.caf
  61. 0 0
      LoopFollow/Resources/Alarm Sounds/Marimba_Flutter_or_Shake.caf
  62. 0 0
      LoopFollow/Resources/Alarm Sounds/Martian_Gun.caf
  63. 0 0
      LoopFollow/Resources/Alarm Sounds/Martian_Scanner.caf
  64. 0 0
      LoopFollow/Resources/Alarm Sounds/Metallic.caf
  65. 0 0
      LoopFollow/Resources/Alarm Sounds/Nightguard.caf
  66. 0 0
      LoopFollow/Resources/Alarm Sounds/Not_Kiddin.caf
  67. 0 0
      LoopFollow/Resources/Alarm Sounds/Open_Your_Eyes_And_See.caf
  68. 0 0
      LoopFollow/Resources/Alarm Sounds/Orchestral_Horns.caf
  69. 0 0
      LoopFollow/Resources/Alarm Sounds/Oringz.caf
  70. 0 0
      LoopFollow/Resources/Alarm Sounds/Pager_Beeps.caf
  71. 0 0
      LoopFollow/Resources/Alarm Sounds/Remembers_Me_Of_Asia.caf
  72. 0 0
      LoopFollow/Resources/Alarm Sounds/Rise_And_Shine.caf
  73. 0 0
      LoopFollow/Resources/Alarm Sounds/Rush.caf
  74. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Air_Raid_Alarm.caf
  75. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm.caf
  76. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_1.caf
  77. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_2.caf
  78. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_3.caf
  79. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_4.caf
  80. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Computer_Console_Alarm.caf
  81. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Console_Alarm.caf
  82. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Eerie_Alarm.caf
  83. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Engine_Shut_Down.caf
  84. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Incoming_Message_Alert.caf
  85. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Spaceship_Message.caf
  86. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Spaceship_Warm_Up.caf
  87. 0 0
      LoopFollow/Resources/Alarm Sounds/Sci-Fi_Warning.caf
  88. 0 0
      LoopFollow/Resources/Alarm Sounds/Signature_Corporate.caf
  89. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Calibration_Needed.caf
  90. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Device_Muted.caf
  91. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Glucose_Dropping_Fast.caf
  92. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Glucose_Rising_Fast.caf
  93. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_High_Glucose.caf
  94. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Low_Glucose.caf
  95. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Missed_Readings.caf
  96. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Transmitter_Battery_Low.caf
  97. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Urgent_High_Glucose.caf
  98. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Alert_Urgent_Low_Glucose.caf
  99. 0 0
      LoopFollow/Resources/Alarm Sounds/Siri_Calibration_Needed.caf
  100. 0 0
      LoopFollow/Alarm Sounds/Siri_Device_Muted.caf

+ 50 - 35
LoopFollow.xcodeproj/project.pbxproj

@@ -8,6 +8,11 @@
 
 /* Begin PBXBuildFile section */
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
+		FC16A97A24996673003D6245 /* NightScout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97924996673003D6245 /* NightScout.swift */; };
+		FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */; };
+		FC16A97D24996747003D6245 /* Alarms.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97C24996747003D6245 /* Alarms.swift */; };
+		FC16A97F249969E2003D6245 /* Graphs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97E249969E2003D6245 /* Graphs.swift */; };
+		FC16A98124996C07003D6245 /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A98024996C07003D6245 /* DateTime.swift */; };
 		FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC688592489554800A0279D /* BackgroundTaskAudio.swift */; };
 		FC5A5C3D2497B229009C550E /* Config.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = FC5A5C3C2497B229009C550E /* Config.xcconfig */; };
 		FC7CE518248ABE37001F83B8 /* Siri_Alert_Calibration_Needed.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4A9248ABE2B001F83B8 /* Siri_Alert_Calibration_Needed.caf */; };
@@ -121,7 +126,6 @@
 		FC7CE584248ABE37001F83B8 /* Oringz.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE515248ABE37001F83B8 /* Oringz.caf */; };
 		FC7CE585248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE516248ABE37001F83B8 /* Siri_Glucose_Dropping_Fast.caf */; };
 		FC7CE586248ABE37001F83B8 /* Martian_Gun.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE517248ABE37001F83B8 /* Martian_Gun.caf */; };
-		FC7CE58A248ABEA3001F83B8 /* AlarmSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */; };
 		FC7CE58D248ABEF2001F83B8 /* alarm.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE58B248ABEF1001F83B8 /* alarm.mp3 */; };
 		FC7CE58E248ABEF2001F83B8 /* alarm-notification.m4a in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE58C248ABEF1001F83B8 /* alarm-notification.m4a */; };
 		FC7CE59C248D33A9001F83B8 /* dragbar.png in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE59B248D33A9001F83B8 /* dragbar.png */; };
@@ -131,7 +135,6 @@
 		FC97881C2485969B00A7906C /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC97881B2485969B00A7906C /* MainViewController.swift */; };
 		FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC97881D2485969B00A7906C /* NightScoutViewController.swift */; };
 		FC9788212485969B00A7906C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC97881F2485969B00A7906C /* Main.storyboard */; };
-		FC9788242485969B00A7906C /* LoopFollow.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FC9788222485969B00A7906C /* LoopFollow.xcdatamodeld */; };
 		FC9788262485969C00A7906C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FC9788252485969C00A7906C /* Assets.xcassets */; };
 		FC9788292485969C00A7906C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC9788272485969C00A7906C /* LaunchScreen.storyboard */; };
 		FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */; };
@@ -153,6 +156,10 @@
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		ECA3EFB4037410B4973BB632 /* Pods-LoopFollow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.debug.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig"; sourceTree = "<group>"; };
+		FC16A97924996673003D6245 /* NightScout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScout.swift; sourceTree = "<group>"; };
+		FC16A97C24996747003D6245 /* Alarms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarms.swift; sourceTree = "<group>"; };
+		FC16A97E249969E2003D6245 /* Graphs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graphs.swift; sourceTree = "<group>"; };
+		FC16A98024996C07003D6245 /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = "<group>"; };
 		FC5A5C3C2497B229009C550E /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
 		FC7CE4A9248ABE2B001F83B8 /* Siri_Alert_Calibration_Needed.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Siri_Alert_Calibration_Needed.caf; sourceTree = "<group>"; };
 		FC7CE4AA248ABE2B001F83B8 /* Rise_And_Shine.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Rise_And_Shine.caf; sourceTree = "<group>"; };
@@ -277,7 +284,6 @@
 		FC97881B2485969B00A7906C /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
 		FC97881D2485969B00A7906C /* NightScoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScoutViewController.swift; sourceTree = "<group>"; };
 		FC9788202485969B00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
-		FC9788232485969B00A7906C /* LoopFollow.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LoopFollow.xcdatamodel; sourceTree = "<group>"; };
 		FC9788252485969C00A7906C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		FC9788282485969C00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryKeyPath.swift; sourceTree = "<group>"; };
@@ -328,6 +334,28 @@
 			path = Pods;
 			sourceTree = "<group>";
 		};
+		FC16A97624995FEE003D6245 /* Application */ = {
+			isa = PBXGroup;
+			children = (
+				FC97881F2485969B00A7906C /* Main.storyboard */,
+				FC9788172485969B00A7906C /* AppDelegate.swift */,
+				FC9788192485969B00A7906C /* SceneDelegate.swift */,
+				FC9788272485969C00A7906C /* LaunchScreen.storyboard */,
+			);
+			path = Application;
+			sourceTree = "<group>";
+		};
+		FC16A9782499657E003D6245 /* Controllers */ = {
+			isa = PBXGroup;
+			children = (
+				FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */,
+				FC16A97924996673003D6245 /* NightScout.swift */,
+				FC16A97C24996747003D6245 /* Alarms.swift */,
+				FC16A97E249969E2003D6245 /* Graphs.swift */,
+			);
+			path = Controllers;
+			sourceTree = "<group>";
+		};
 		FC7CE4A8248ABE0D001F83B8 /* Alarm Sounds */ = {
 			isa = PBXGroup;
 			children = (
@@ -451,6 +479,10 @@
 		FC7CE59A248D334B001F83B8 /* Resources */ = {
 			isa = PBXGroup;
 			children = (
+				FCC6885D24896A6C00A0279D /* silence.mp3 */,
+				FCC6885B2489559400A0279D /* blank.wav */,
+				FC7CE4A8248ABE0D001F83B8 /* Alarm Sounds */,
+				FC9788252485969C00A7906C /* Assets.xcassets */,
 				FC7CE59B248D33A9001F83B8 /* dragbar.png */,
 			);
 			path = Resources;
@@ -459,20 +491,15 @@
 		FC8DEEE32485D1680075863F /* LoopFollow */ = {
 			isa = PBXGroup;
 			children = (
+				FCC688702489A57C00A0279D /* Loop Follow.entitlements */,
+				FC8DEEE62485D1ED0075863F /* Info.plist */,
 				FC7CE59A248D334B001F83B8 /* Resources */,
-				FC7CE4A8248ABE0D001F83B8 /* Alarm Sounds */,
-				FCC68871248A736700A0279D /* Controllers */,
+				FCC68871248A736700A0279D /* ViewControllers */,
+				FC16A9782499657E003D6245 /* Controllers */,
 				FCC688542489367300A0279D /* helpers */,
 				FCC68872248A748A00A0279D /* Models */,
-				FCC688702489A57C00A0279D /* Loop Follow.entitlements */,
 				FCC688512489363F00A0279D /* repository */,
-				FC8DEEE62485D1ED0075863F /* Info.plist */,
-				FC9788172485969B00A7906C /* AppDelegate.swift */,
-				FC9788192485969B00A7906C /* SceneDelegate.swift */,
-				FC97881F2485969B00A7906C /* Main.storyboard */,
-				FC9788252485969C00A7906C /* Assets.xcassets */,
-				FC9788272485969C00A7906C /* LaunchScreen.storyboard */,
-				FC9788222485969B00A7906C /* LoopFollow.xcdatamodeld */,
+				FC16A97624995FEE003D6245 /* Application */,
 			);
 			path = LoopFollow;
 			sourceTree = "<group>";
@@ -499,6 +526,8 @@
 		FCC688512489363F00A0279D /* repository */ = {
 			isa = PBXGroup;
 			children = (
+				FCC6886624898F8000A0279D /* UserDefaultsValue.swift */,
+				FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */,
 				FCC6886424898EEE00A0279D /* UserDefaults.swift */,
 			);
 			path = repository;
@@ -508,20 +537,17 @@
 			isa = PBXGroup;
 			children = (
 				FCC6886E2489A53800A0279D /* AppConstants.swift */,
-				FCC6886624898F8000A0279D /* UserDefaultsValue.swift */,
-				FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */,
 				FCC6886A24898FD800A0279D /* ObservationToken.swift */,
 				FCC6886C2489909D00A0279D /* AnyConvertible.swift */,
-				FCC6885D24896A6C00A0279D /* silence.mp3 */,
-				FCC6885B2489559400A0279D /* blank.wav */,
 				FCC688592489554800A0279D /* BackgroundTaskAudio.swift */,
 				FCFEEC9F2488157B00402A7F /* Chart.swift */,
 				FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */,
+				FC16A98024996C07003D6245 /* DateTime.swift */,
 			);
 			path = helpers;
 			sourceTree = "<group>";
 		};
-		FCC68871248A736700A0279D /* Controllers */ = {
+		FCC68871248A736700A0279D /* ViewControllers */ = {
 			isa = PBXGroup;
 			children = (
 				FC97881B2485969B00A7906C /* MainViewController.swift */,
@@ -530,13 +556,12 @@
 				FCC6884F248935D800A0279D /* AlarmViewController.swift */,
 				FC7CE59E248D8D23001F83B8 /* SnoozeViewController.swift */,
 			);
-			path = Controllers;
+			path = ViewControllers;
 			sourceTree = "<group>";
 		};
 		FCC68872248A748A00A0279D /* Models */ = {
 			isa = PBXGroup;
 			children = (
-				FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -778,18 +803,21 @@
 				FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */,
 				FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,
+				FC16A97A24996673003D6245 /* NightScout.swift in Sources */,
 				FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */,
-				FC7CE58A248ABEA3001F83B8 /* AlarmSound.swift in Sources */,
-				FC9788242485969B00A7906C /* LoopFollow.xcdatamodeld in Sources */,
+				FC16A97D24996747003D6245 /* Alarms.swift in Sources */,
+				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,
 				FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */,
 				FCFEECA02488157B00402A7F /* Chart.swift in Sources */,
 				FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */,
+				FC16A97F249969E2003D6245 /* Graphs.swift in Sources */,
 				FC97881C2485969B00A7906C /* MainViewController.swift in Sources */,
 				FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */,
 				FC9788182485969B00A7906C /* AppDelegate.swift in Sources */,
 				FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,
+				FC16A98124996C07003D6245 /* DateTime.swift in Sources */,
 				FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -992,19 +1020,6 @@
 			defaultConfigurationName = Release;
 		};
 /* End XCConfigurationList section */
-
-/* Begin XCVersionGroup section */
-		FC9788222485969B00A7906C /* LoopFollow.xcdatamodeld */ = {
-			isa = XCVersionGroup;
-			children = (
-				FC9788232485969B00A7906C /* LoopFollow.xcdatamodel */,
-			);
-			currentVersion = FC9788232485969B00A7906C /* LoopFollow.xcdatamodel */;
-			path = LoopFollow.xcdatamodeld;
-			sourceTree = "<group>";
-			versionGroupType = wrapper.xcdatamodel;
-		};
-/* End XCVersionGroup section */
 	};
 	rootObject = FC97880C2485969B00A7906C /* Project object */;
 }

LoopFollow/AppDelegate.swift → LoopFollow/Application/AppDelegate.swift


LoopFollow/Base.lproj/LaunchScreen.storyboard → LoopFollow/Application/Base.lproj/LaunchScreen.storyboard


+ 13 - 2
LoopFollow/Base.lproj/Main.storyboard

@@ -103,9 +103,16 @@
                                 </subviews>
                             </stackView>
                             <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="51B-JU-npv" customClass="LineChartView" customModule="Charts">
-                                <rect key="frame" x="8" y="312" width="398" height="425"/>
+                                <rect key="frame" x="8" y="312" width="398" height="297"/>
                                 <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
                             </view>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AOf-kb-6CI" userLabel="BasalChart" customClass="LineChartView" customModule="Charts">
+                                <rect key="frame" x="8" y="617" width="398" height="120"/>
+                                <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" priority="750" constant="120" id="qWe-ZM-8HL"/>
+                                </constraints>
+                            </view>
                             <view autoresizesSubviews="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nml-lr-dgQ" userLabel="BGChartFull" customClass="LineChartView" customModule="Charts">
                                 <rect key="frame" x="8" y="745" width="398" height="60"/>
                                 <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
@@ -118,13 +125,16 @@
                         <constraints>
                             <constraint firstItem="51B-JU-npv" firstAttribute="top" secondItem="mJy-Ly-Jze" secondAttribute="bottom" constant="8" id="2Qj-m5-4Yj"/>
                             <constraint firstItem="PQr-Ze-W5v" firstAttribute="trailing" secondItem="Nml-lr-dgQ" secondAttribute="trailing" constant="8" id="5dn-w0-XWn"/>
+                            <constraint firstItem="AOf-kb-6CI" firstAttribute="top" secondItem="51B-JU-npv" secondAttribute="bottom" constant="8" id="8aC-1I-XGc"/>
                             <constraint firstItem="Nml-lr-dgQ" firstAttribute="leading" secondItem="PQr-Ze-W5v" secondAttribute="leading" constant="8" id="D6D-3r-YIy"/>
+                            <constraint firstItem="AOf-kb-6CI" firstAttribute="leading" secondItem="PQr-Ze-W5v" secondAttribute="leading" constant="8" id="KGd-6a-HA1"/>
                             <constraint firstItem="PQr-Ze-W5v" firstAttribute="trailing" secondItem="mJy-Ly-Jze" secondAttribute="trailing" constant="8" id="KcX-yW-Vnd"/>
                             <constraint firstItem="PQr-Ze-W5v" firstAttribute="bottom" secondItem="Nml-lr-dgQ" secondAttribute="bottom" constant="8" id="Sb3-4z-Qup"/>
                             <constraint firstItem="mJy-Ly-Jze" firstAttribute="leading" secondItem="PQr-Ze-W5v" secondAttribute="leading" constant="8" id="b30-Y0-qsu"/>
                             <constraint firstItem="51B-JU-npv" firstAttribute="leading" secondItem="PQr-Ze-W5v" secondAttribute="leading" constant="8" id="dfE-Q6-SML"/>
-                            <constraint firstItem="Nml-lr-dgQ" firstAttribute="top" secondItem="51B-JU-npv" secondAttribute="bottom" constant="8" id="hdx-AR-c3V"/>
+                            <constraint firstItem="Nml-lr-dgQ" firstAttribute="top" secondItem="AOf-kb-6CI" secondAttribute="bottom" constant="8" id="oLY-NQ-TFm"/>
                             <constraint firstItem="PQr-Ze-W5v" firstAttribute="trailing" secondItem="51B-JU-npv" secondAttribute="trailing" constant="8" id="oqO-3J-e8a"/>
+                            <constraint firstItem="PQr-Ze-W5v" firstAttribute="trailing" secondItem="AOf-kb-6CI" secondAttribute="trailing" constant="8" id="xBV-Eb-pEs"/>
                             <constraint firstItem="mJy-Ly-Jze" firstAttribute="top" secondItem="PQr-Ze-W5v" secondAttribute="top" constant="8" id="xem-Lc-bdU"/>
                         </constraints>
                         <viewLayoutGuide key="safeArea" id="PQr-Ze-W5v"/>
@@ -134,6 +144,7 @@
                         <outlet property="BGChart" destination="51B-JU-npv" id="oF1-UA-KHE"/>
                         <outlet property="BGChartFull" destination="Nml-lr-dgQ" id="Z21-fG-GK9"/>
                         <outlet property="BGText" destination="vXU-IW-GOL" id="2aC-pv-Qbg"/>
+                        <outlet property="BasalChart" destination="AOf-kb-6CI" id="Qsl-jz-Qfj"/>
                         <outlet property="DeltaText" destination="sb7-g9-JNv" id="h7s-Yj-i4M"/>
                         <outlet property="DirectionText" destination="4sd-6h-LSj" id="lB5-Z0-p25"/>
                         <outlet property="LoopStatusLabel" destination="XEm-uK-Xui" id="EBm-Ta-tZ1"/>

LoopFollow/SceneDelegate.swift → LoopFollow/Application/SceneDelegate.swift


LoopFollow/Models/AlarmSound.swift → LoopFollow/Controllers/AlarmSound.swift


+ 322 - 0
LoopFollow/Controllers/Alarms.swift

@@ -0,0 +1,322 @@
+//
+//  Alarms.swift
+//  LoopFollow
+//
+//  Created by Jon Fawcett on 6/16/20.
+//  Copyright © 2020 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+
+extension MainViewController {
+    
+    
+    func checkAlarms(bgs: [sgvData]) {
+           
+           // Don't check or fire alarms within 1 minute of prior alarm
+           if checkAlarmTimer.isValid {  return }
+           
+           let date = Date()
+           let now = date.timeIntervalSince1970
+           let currentBG = bgs[bgs.count - 1].sgv
+           let lastBG = bgs[bgs.count - 2].sgv
+           guard let deltas: [Int] = [
+               bgs[bgs.count - 1].sgv - bgs[bgs.count - 2].sgv,
+               bgs[bgs.count - 2].sgv - bgs[bgs.count - 3].sgv,
+               bgs[bgs.count - 3].sgv - bgs[bgs.count - 4].sgv
+               ] else {}
+           let currentBGTime = bgs[bgs.count - 1].date
+           var alarmTriggered = false
+           
+           clearOldSnoozes()
+           
+           // Exit if all is snoozed
+           // still send persistent notification with all snoozed
+           if UserDefaultsRepository.alertSnoozeAllIsSnoozed.value {
+               persistentNotification(bgTime: currentBGTime)
+               return
+           }
+           
+           
+           // BG Based Alarms
+           // Check to make sure it is a current reading and has not already triggered alarm from this reading
+           if now - currentBGTime <= (5*60) && currentBGTime > UserDefaultsRepository.snoozedBGReadingTime.value as! TimeInterval {
+               
+               // trigger temporary alert first
+               if UserDefaultsRepository.alertTemporaryActive.value {
+                   if UserDefaultsRepository.alertTemporaryBelow.value {
+                       if currentBG < UserDefaultsRepository.alertTemporaryBG.value {
+                           UserDefaultsRepository.alertTemporaryActive.value = false
+                           AlarmSound.whichAlarm = "Temporary Alert"
+                           triggerAlarm(sound: UserDefaultsRepository.alertTemporarySound.value, snooozedBGReadingTime: currentBGTime)
+                           return
+                       }
+                   } else{
+                       if currentBG > UserDefaultsRepository.alertTemporaryBG.value {
+                         tabBarController?.selectedIndex = 2
+                           AlarmSound.whichAlarm = "Temporary Alert"
+                           triggerAlarm(sound: UserDefaultsRepository.alertTemporarySound.value, snooozedBGReadingTime: currentBGTime)
+                           return
+                      }
+                   }
+               }
+               
+               // Check Urgent Low
+               if UserDefaultsRepository.alertUrgentLowActive.value && !UserDefaultsRepository.alertUrgentLowIsSnoozed.value &&
+               currentBG <= UserDefaultsRepository.alertUrgentLowBG.value {
+                   // Separating this makes it so the low or drop alerts won't trigger if they already snoozed the urgent low
+                   if !UserDefaultsRepository.alertUrgentLowIsSnoozed.value {
+                       AlarmSound.whichAlarm = "Urgent Low Alert"
+                       triggerAlarm(sound: UserDefaultsRepository.alertUrgentLowSound.value, snooozedBGReadingTime: currentBGTime)
+                       return
+                   } else {
+                       return
+                   }
+               }
+               
+               // Check Low
+               if UserDefaultsRepository.alertLowActive.value && !UserDefaultsRepository.alertUrgentLowIsSnoozed.value &&
+               currentBG <= UserDefaultsRepository.alertLowBG.value && !UserDefaultsRepository.alertLowIsSnoozed.value {
+                   AlarmSound.whichAlarm = "Low Alert"
+                   triggerAlarm(sound: UserDefaultsRepository.alertLowSound.value, snooozedBGReadingTime: currentBGTime)
+                   return
+               }
+               
+               // Check Urgent High
+               if UserDefaultsRepository.alertUrgentHighActive.value && !UserDefaultsRepository.alertUrgentHighIsSnoozed.value &&
+               currentBG >= UserDefaultsRepository.alertUrgentHighBG.value {
+                   // Separating this makes it so the high or rise alerts won't trigger if they already snoozed the urgent high
+                   if !UserDefaultsRepository.alertUrgentHighIsSnoozed.value {
+                           AlarmSound.whichAlarm = "Urgent High Alert"
+                           triggerAlarm(sound: UserDefaultsRepository.alertUrgentHighSound.value, snooozedBGReadingTime: currentBGTime)
+                           return
+                   } else {
+                       return
+                   }
+                   
+               }
+               
+               // Check High
+               let persistentReadings = Int(UserDefaultsRepository.alertHighPersistent.value / 5)
+               let persistentBG = bgData[bgData.count - 1 - persistentReadings].sgv
+               if UserDefaultsRepository.alertHighActive.value &&
+                   !UserDefaultsRepository.alertHighIsSnoozed.value &&
+                   currentBG >= UserDefaultsRepository.alertHighBG.value &&
+                   persistentBG >= UserDefaultsRepository.alertHighBG.value &&
+                   !UserDefaultsRepository.alertHighIsSnoozed.value {
+                       AlarmSound.whichAlarm = "High Alert"
+                       triggerAlarm(sound: UserDefaultsRepository.alertHighSound.value, snooozedBGReadingTime: currentBGTime)
+                       return
+               }
+               
+               
+               
+               // Check Fast Drop
+               if UserDefaultsRepository.alertFastDropActive.value && !UserDefaultsRepository.alertFastDropIsSnoozed.value {
+                   // make sure limit is off or BG is below value
+                   if (!UserDefaultsRepository.alertFastDropUseLimit.value) || (UserDefaultsRepository.alertFastDropUseLimit.value && currentBG < UserDefaultsRepository.alertFastDropBelowBG.value) {
+                       let compare = 0 - UserDefaultsRepository.alertFastDropDelta.value
+                       
+                       //check last 2/3/4 readings
+                       if (UserDefaultsRepository.alertFastDropReadings.value == 2 && deltas[0] <= compare)
+                       || (UserDefaultsRepository.alertFastDropReadings.value == 3 && deltas[0] <= compare && deltas[1] <= compare)
+                       || (UserDefaultsRepository.alertFastDropReadings.value == 4 && deltas[0] <= compare && deltas[1] <= compare && deltas[2] <= compare) {
+                           AlarmSound.whichAlarm = "Fast Drop Alert"
+                           triggerAlarm(sound: UserDefaultsRepository.alertFastDropSound.value, snooozedBGReadingTime: currentBGTime)
+                           return
+                       }
+                   }
+               }
+               
+               // Check Fast Rise
+               if UserDefaultsRepository.alertFastRiseActive.value && !UserDefaultsRepository.alertFastRiseIsSnoozed.value {
+                   // make sure limit is off or BG is above value
+                   if (!UserDefaultsRepository.alertFastRiseUseLimit.value) || (UserDefaultsRepository.alertFastRiseUseLimit.value && currentBG > UserDefaultsRepository.alertFastRiseAboveBG.value) {
+                       let compare = UserDefaultsRepository.alertFastDropDelta.value
+                       
+                       //check last 2/3/4 readings
+                       if (UserDefaultsRepository.alertFastRiseReadings.value == 2 && deltas[0] >= compare)
+                       || (UserDefaultsRepository.alertFastRiseReadings.value == 3 && deltas[0] >= compare && deltas[1] >= compare)
+                       || (UserDefaultsRepository.alertFastRiseReadings.value == 4 && deltas[0] >= compare && deltas[1] >= compare && deltas[2] >= compare) {
+                           AlarmSound.whichAlarm = "Fast Rise Alert"
+                           triggerAlarm(sound: UserDefaultsRepository.alertFastRiseSound.value, snooozedBGReadingTime: currentBGTime)
+                           return
+                       }
+                   }
+               }
+               
+
+               
+           }
+           
+           // These only get checked and fire if a BG reading doesn't fire
+           if UserDefaultsRepository.alertNotLoopingActive.value
+               && !UserDefaultsRepository.alertNotLoopingIsSnoozed.value
+               && (Double(now - UserDefaultsRepository.alertLastLoopTime.value) >= Double(UserDefaultsRepository.alertNotLooping.value * 60))
+               && UserDefaultsRepository.alertLastLoopTime.value > 0 {
+               
+               var trigger = true
+               if (UserDefaultsRepository.alertNotLoopingUseLimits.value
+                   && (
+                       (currentBG <= UserDefaultsRepository.alertNotLoopingUpperLimit.value
+                       && currentBG >= UserDefaultsRepository.alertNotLoopingLowerLimit.value) ||
+                       // Ignore Limits if BG reading is older than non looping time
+                       (Double(now - currentBGTime) >= Double(UserDefaultsRepository.alertNotLooping.value * 60))
+                   ) ||
+                   !UserDefaultsRepository.alertNotLoopingUseLimits.value) {
+                       AlarmSound.whichAlarm = "Not Looping Alert"
+                       triggerAlarm(sound: UserDefaultsRepository.alertNotLoopingSound.value, snooozedBGReadingTime: nil)
+                       return
+               }
+           }
+           
+           //check for missed reading alert
+           if UserDefaultsRepository.alertMissedReadingActive.value && !UserDefaultsRepository.alertMissedReadingIsSnoozed.value && (Double(now - currentBGTime) >= Double(UserDefaultsRepository.alertMissedReading.value * 60)) {
+               AlarmSound.whichAlarm = "Missed Reading Alert"
+                   triggerAlarm(sound: UserDefaultsRepository.alertMissedReadingSound.value, snooozedBGReadingTime: nil)
+                   return
+           }
+           
+           // Check Sage
+           if UserDefaultsRepository.alertSAGEActive.value && !UserDefaultsRepository.alertSAGEIsSnoozed.value {
+               let insertTime = Double(UserDefaultsRepository.alertSageInsertTime.value)
+               let alertDistance = Double(UserDefaultsRepository.alertSAGE.value * 60 * 60)
+               let delta = now - insertTime
+               let tenDays = 10 * 24 * 60 * 60
+               if Double(tenDays) - Double(delta) <= alertDistance {
+                   AlarmSound.whichAlarm = "Sensor Change Alert"
+                   triggerAlarm(sound: UserDefaultsRepository.alertSAGESound.value, snooozedBGReadingTime: nil)
+                   return
+               }
+           }
+           
+           // Check Cage
+           if UserDefaultsRepository.alertCAGEActive.value && !UserDefaultsRepository.alertCAGEIsSnoozed.value {
+               let insertTime = Double(UserDefaultsRepository.alertCageInsertTime.value)
+               let alertDistance = Double(UserDefaultsRepository.alertCAGE.value * 60 * 60)
+               let delta = now - insertTime
+               let tenDays = 3 * 24 * 60 * 60
+               if Double(tenDays) - Double(delta) <= alertDistance {
+                   AlarmSound.whichAlarm = "Pump Change Alert"
+                   triggerAlarm(sound: UserDefaultsRepository.alertCAGESound.value, snooozedBGReadingTime: nil)
+                   return
+               }
+           }
+           
+           // still send persistent notification if no alarms trigger and persistent notification is on
+           persistentNotification(bgTime: currentBGTime)
+           
+       }
+       
+    
+    func triggerAlarm(sound: String, snooozedBGReadingTime: TimeInterval?)
+    {
+        guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
+        snoozer.updateDisplayWhenTriggered(bgVal: BGText.text ?? "", directionVal: DirectionText.text ?? "", deltaVal: DeltaText.text ?? "", minAgoVal: MinAgoText.text ?? "", alertLabelVal: AlarmSound.whichAlarm)
+        snoozeTabItem.isEnabled = true;
+        tabBarController?.selectedIndex = 2
+        if snooozedBGReadingTime != nil {
+            UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime
+        }
+        AlarmSound.setSoundFile(str: sound)
+        AlarmSound.play()
+    }
+    
+    func clearOldSnoozes(){
+          let date = Date()
+          let now = date.timeIntervalSince1970
+          var needsReload: Bool = false
+          guard let alarms = self.tabBarController!.viewControllers?[1] as? AlarmViewController else { return }
+          
+          if date > UserDefaultsRepository.alertSnoozeAllTime.value ?? date {
+              UserDefaultsRepository.alertSnoozeAllTime.setNil(key: "alertSnoozeAllTime")
+              UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertSnoozeAllTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertSnoozeAllIsSnoozed", value: false)
+            
+          }
+          
+          if date > UserDefaultsRepository.alertUrgentLowSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertUrgentLowSnoozedTime.setNil(key: "alertUrgentLowSnoozedTime")
+              UserDefaultsRepository.alertUrgentLowIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertUrgentLowSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertUrgentLowIsSnoozed", value: false)
+            
+          }
+          if date > UserDefaultsRepository.alertLowSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertLowSnoozedTime.setNil(key: "alertLowSnoozedTime")
+              UserDefaultsRepository.alertLowIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertLowSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertLowIsSnoozed", value: false)
+         
+          }
+          if date > UserDefaultsRepository.alertHighSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertHighSnoozedTime.setNil(key: "alertHighSnoozedTime")
+              UserDefaultsRepository.alertHighIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertHighSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertHighIsSnoozed", value: false)
+            
+          }
+          if date > UserDefaultsRepository.alertUrgentHighSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertUrgentHighSnoozedTime.setNil(key: "alertUrgentHighSnoozedTime")
+              UserDefaultsRepository.alertUrgentHighIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertUrgentHighSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertUrgentHighIsSnoozed", value: false)
+            
+          }
+          if date > UserDefaultsRepository.alertFastDropSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertFastDropSnoozedTime.setNil(key: "alertFastDropSnoozedTime")
+              UserDefaultsRepository.alertFastDropIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertFastDropSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertFastDropIsSnoozed", value: false)
+             
+          }
+          if date > UserDefaultsRepository.alertFastRiseSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertFastRiseSnoozedTime.setNil(key: "alertFastRiseSnoozedTime")
+              UserDefaultsRepository.alertFastRiseIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertFastRiseSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertFastRiseIsSnoozed", value: false)
+             
+          }
+          if date > UserDefaultsRepository.alertMissedReadingSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertMissedReadingSnoozedTime.setNil(key: "alertMissedReadingSnoozedTime")
+              UserDefaultsRepository.alertMissedReadingIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertMissedReadingSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertMissedReadingIsSnoozed", value: false)
+            
+          }
+          if date > UserDefaultsRepository.alertNotLoopingSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertNotLoopingSnoozedTime.setNil(key: "alertNotLoopingSnoozedTime")
+              UserDefaultsRepository.alertNotLoopingIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertNotLoopingSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertNotLoopingIsSnoozed", value: false)
+           
+              
+          }
+          if date > UserDefaultsRepository.alertMissedBolusSnoozedTime.value ?? date {
+              UserDefaultsRepository.alertMissedBolusSnoozedTime.setNil(key: "alertMissedBolusSnoozedTime")
+              UserDefaultsRepository.alertMissedBolusIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertMissedBolusSnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertMissedBolusIsSnoozed", value: false)
+
+          }
+          if date > UserDefaultsRepository.alertSAGESnoozedTime.value ?? date {
+              UserDefaultsRepository.alertSAGESnoozedTime.setNil(key: "alertSAGESnoozedTime")
+              UserDefaultsRepository.alertSAGEIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertSAGESnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertSAGEIsSnoozed", value: false)
+    
+          }
+          if date > UserDefaultsRepository.alertCAGESnoozedTime.value ?? date {
+              UserDefaultsRepository.alertCAGESnoozedTime.setNil(key: "alertCAGESnoozedTime")
+              UserDefaultsRepository.alertCAGEIsSnoozed.value = false
+              alarms.reloadSnoozeTime(key: "alertCAGESnoozedTime", setNil: true)
+              alarms.reloadIsSnoozed(key: "alertCAGEIsSnoozed", value: false)
+
+          }
+
+          
+      }
+    
+}

+ 239 - 0
LoopFollow/Controllers/Graphs.swift

@@ -0,0 +1,239 @@
+//
+//  Graphs.swift
+//  LoopFollow
+//
+//  Created by Jon Fawcett on 6/16/20.
+//  Copyright © 2020 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import Charts
+import UIKit
+
+
+extension MainViewController {
+    
+    // rewrite this func
+    func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) {
+        print("chartTranslated")
+        
+        if  chartView == BGChart {
+            let currentMatrix = chartView.viewPortHandler.touchMatrix
+            BasalChart.viewPortHandler.refresh(newMatrix: currentMatrix, chart: BasalChart, invalidate: true)
+        }else {
+            let currentMatrix = BasalChart.viewPortHandler.touchMatrix
+            BGChart.viewPortHandler.refresh(newMatrix: currentMatrix, chart: BGChart, invalidate: true)
+        }
+    }
+    
+    func createGraph(entries: [sgvData]){
+        var bgChartEntry = [ChartDataEntry]()
+        var colors = [NSUIColor]()
+        var maxBG: Int = 250
+        for i in 0..<entries.count{
+            var dateString = String(entries[i].date).prefix(10)
+            let dateSecondsOnly = Double(String(dateString))!
+            if entries[i].sgv > maxBG {
+                maxBG = entries[i].sgv
+            }
+            let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv))
+            bgChartEntry.append(value)
+            
+            if Double(entries[i].sgv) >= Double(UserDefaultsRepository.highLine.value) {
+                colors.append(NSUIColor.systemYellow)
+            } else if Double(entries[i].sgv) <= Double(UserDefaultsRepository.lowLine.value) {
+                colors.append(NSUIColor.systemRed)
+            } else {
+                colors.append(NSUIColor.systemGreen)
+            }
+        }
+        
+        // Add Prediction Data
+        if predictionData.count > 0 {
+            var startingTime = bgChartEntry[bgChartEntry.count - 1].x + 300
+            var i = 0
+            // Add 1 hour of predictions
+            while i < 12 {
+                var predictionVal = Double(predictionData[i])
+                // Below can be turned on to prevent out of range on the graph if desired.
+                // It currently just drops them out of view
+                if predictionVal > 400 {
+               //     predictionVal = 400
+                } else if predictionVal < 0 {
+                //    predictionVal = 0
+                }
+                let value = ChartDataEntry(x: startingTime + 5, y: predictionVal)
+                bgChartEntry.append(value)
+                colors.append(NSUIColor.systemPurple)
+                startingTime += 300
+                i += 1
+            }
+        }
+        
+        let line1 = LineChartDataSet(entries:bgChartEntry, label: "")
+        line1.circleRadius = 3
+        line1.circleColors = [NSUIColor.systemGreen]
+        line1.drawCircleHoleEnabled = false
+        if UserDefaultsRepository.showLines.value {
+            line1.lineWidth = 2
+        } else {
+            line1.lineWidth = 0
+        }
+        if UserDefaultsRepository.showDots.value {
+            line1.drawCirclesEnabled = true
+        } else {
+            line1.drawCirclesEnabled = false
+        }
+        line1.setDrawHighlightIndicators(false)
+        line1.valueFont.withSize(50)
+        
+        for i in 1..<colors.count{
+            line1.addColor(colors[i])
+            line1.circleColors.append(colors[i])
+        }
+        
+        let data = LineChartData()
+        data.addDataSet(line1)
+        data.setValueFont(UIFont(name: UIFont.systemFont(ofSize: 10).fontName, size: 10)!)
+        data.setDrawValues(false)
+        
+        // Clear limit lines so they don't add multiples when changing the settings
+        BGChart.rightAxis.removeAllLimitLines()
+        
+        //Add lower red line based on low alert value
+        let ll = ChartLimitLine()
+        ll.limit = Double(UserDefaultsRepository.lowLine.value)
+        ll.lineColor = NSUIColor.systemRed
+        BGChart.rightAxis.addLimitLine(ll)
+        
+        //Add upper yellow line based on low alert value
+        let ul = ChartLimitLine()
+        ul.limit = Double(UserDefaultsRepository.highLine.value)
+        ul.lineColor = NSUIColor.systemYellow
+        BGChart.rightAxis.addLimitLine(ul)
+        
+        BGChart.xAxis.valueFormatter = ChartXValueFormatter()
+        BGChart.xAxis.granularity = 1800
+        BGChart.xAxis.labelTextColor = NSUIColor.label
+        BGChart.xAxis.labelPosition = XAxis.LabelPosition.bottom
+        BGChart.rightAxis.labelTextColor = NSUIColor.label
+        BGChart.rightAxis.labelPosition = YAxis.LabelPosition.insideChart
+        BGChart.rightAxis.axisMinimum = 40
+        BGChart.leftAxis.axisMinimum = 40
+        BGChart.rightAxis.axisMaximum = Double(maxBG)
+        BGChart.leftAxis.axisMaximum = Double(maxBG)
+        BGChart.leftAxis.enabled = false
+        BGChart.legend.enabled = false
+        BGChart.scaleYEnabled = false
+        BGChart.data = data
+        BGChart.setExtraOffsets(left: 10, top: 10, right: 10, bottom: 10)
+        BGChart.setVisibleXRangeMinimum(10)
+        BGChart.drawGridBackgroundEnabled = true
+        BGChart.gridBackgroundColor = NSUIColor.secondarySystemBackground
+        if firstGraphLoad {
+            BGChart.zoom(scaleX: 18, scaleY: 1, x: 1, y: 1)
+            firstGraphLoad = false
+        }
+        // 7000 only shows 30 minutes of the hour predictions, leaving the rest on the right of the screen requiring a scroll
+        BGChart.moveViewToX(Date().timeIntervalSince1970)
+
+        
+        createSmallBGGraph(bgChartEntry: bgChartEntry, colors: colors)
+      
+    }
+    
+    func createSmallBGGraph(bgChartEntry: [ChartDataEntry], colors: [NSUIColor]){
+        //24 Hour Small Graph
+        let line2 = LineChartDataSet(entries:bgChartEntry, label: "Number")
+        line2.drawCirclesEnabled = false
+        line2.setDrawHighlightIndicators(false)
+        line2.lineWidth = 1
+        for i in 1..<colors.count{
+            line2.addColor(colors[i])
+            line2.circleColors.append(colors[i])
+        }
+        
+        let data2 = LineChartData()
+        data2.addDataSet(line2)
+        BGChartFull.leftAxis.enabled = false
+        BGChartFull.rightAxis.enabled = false
+        BGChartFull.xAxis.enabled = false
+        BGChartFull.legend.enabled = false
+        BGChartFull.scaleYEnabled = false
+        BGChartFull.scaleXEnabled = false
+        BGChartFull.drawGridBackgroundEnabled = false
+        BGChartFull.data = data2
+    }
+    
+    func createBasalGraph(entries: [basalGraphStruct]){
+        var chartEntry = [ChartDataEntry]()
+        for i in 0..<entries.count{
+            let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].basalRate))
+            chartEntry.append(value)
+        }
+        let line1 = LineChartDataSet(entries:chartEntry, label: "")
+        line1.circleRadius = 3
+        line1.circleColors = [NSUIColor.systemBlue]
+        line1.drawCircleHoleEnabled = false
+        line1.setDrawHighlightIndicators(false)
+        line1.setColor(NSUIColor.systemBlue, alpha: 1.0)
+        line1.lineWidth = 3
+        line1.drawFilledEnabled = true
+        line1.fillColor = NSUIColor.systemBlue
+        line1.drawCirclesEnabled = false
+        let data = LineChartData()
+        data.addDataSet(line1)
+        data.setValueFont(UIFont(name: UIFont.systemFont(ofSize: 10).fontName, size: 10)!)
+        data.setDrawValues(false)
+        
+        BasalChart.xAxis.valueFormatter = ChartXValueFormatter()
+        BasalChart.xAxis.granularity = 1800
+        BasalChart.xAxis.labelTextColor = NSUIColor.label
+        BasalChart.xAxis.labelPosition = XAxis.LabelPosition.bottom
+        BasalChart.rightAxis.labelTextColor = NSUIColor.label
+        BasalChart.rightAxis.labelPosition = YAxis.LabelPosition.insideChart
+        BasalChart.leftAxis.enabled = false
+        BasalChart.legend.enabled = false
+        BasalChart.scaleYEnabled = false
+        BasalChart.data = data
+        BasalChart.setExtraOffsets(left: 10, top: 10, right: 10, bottom: 10)
+        BasalChart.setVisibleXRangeMinimum(10)
+        BasalChart.drawGridBackgroundEnabled = true
+        BasalChart.gridBackgroundColor = NSUIColor.secondarySystemBackground
+        if firstBasalGraphLoad {
+            BasalChart.zoom(scaleX: 18, scaleY: 1, x: 1, y: 1)
+            firstBasalGraphLoad = false
+        }
+        // 7000 only shows 30 minutes of the hour predictions, leaving the rest on the right of the screen requiring a scroll
+        BasalChart.moveViewToX(Date().timeIntervalSince1970)
+        
+        // Bar Chart Build
+        /*var chartEntry = [BarChartDataEntry]()
+        for i in 0..<entries.count{
+            let value = BarChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].basalRate))
+            chartEntry.append(value)
+        }
+        let bar1 = BarChartDataSet(entries:chartEntry, label: "")
+        bar1.setColor(NSUIColor.systemBlue, alpha: 0.5)
+        let data = BarChartData()
+        data.addDataSet(bar1)
+        data.setValueFont(UIFont(name: UIFont.systemFont(ofSize: 10).fontName, size: 10)!)
+        data
+        data.barWidth = 290
+        BasalChart.xAxis.valueFormatter = ChartXValueFormatter()
+        BasalChart.xAxis.granularity = 1800
+        BasalChart.data = data
+        BasalChart.legend.enabled = false
+        BasalChart.xAxis.drawGridLinesEnabled = false
+        BasalChart.leftAxis.drawGridLinesEnabled = false
+        BasalChart.rightAxis.drawGridLinesEnabled = false
+        BasalChart.leftAxis.enabled = false
+        BasalChart.scaleYEnabled = false
+        if firstBasalGraphLoad {
+            BasalChart.zoom(scaleX: 18, scaleY: 1, x: 1, y: 1)
+            firstBasalGraphLoad = false
+        }
+        BasalChart.moveViewToX(BGChart.chartXMax)
+        */
+    }
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 1456
LoopFollow/Controllers/MainViewController.swift


+ 771 - 0
LoopFollow/Controllers/NightScout.swift

@@ -0,0 +1,771 @@
+//
+//  NightScout.swift
+//  LoopFollow
+//
+//  Created by Jon Fawcett on 6/16/20.
+//  Copyright © 2020 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+
+extension MainViewController {
+    
+    // Main loader for all data
+    func nightscoutLoader(forceLoad: Bool = false) {
+        
+        
+        var needsLoaded: Bool = false
+        var onlyPullLastRecord = false
+        
+        // If we have existing data and it's within 5 minutes, we aren't going to do a network call
+        if bgData.count > 0 {
+            let now = NSDate().timeIntervalSince1970
+            let lastReadingTime = bgData[bgData.count - 1].date
+            let secondsAgo = now - lastReadingTime
+            if secondsAgo >= 5*60 {
+                needsLoaded = true
+                if secondsAgo < 10*60 {
+                    onlyPullLastRecord = true
+                }
+            }
+        } else {
+            needsLoaded = true
+        }
+        
+        if forceLoad { needsLoaded = true}
+        // Only do the network calls if we don't have a current reading
+        if needsLoaded {
+            loadProfile(urlUser: urlUser)
+            loadTempBasals(urlUser: urlUser)
+            loadDeviceStatus(urlUser: urlUser)
+            loadBGData(urlUser: urlUser, onlyPullLastRecord: onlyPullLastRecord)
+            clearLastInfoData()
+            loadCage(urlUser: urlUser)
+            loadSage(urlUser: urlUser)
+            
+           // loadBoluses(urlUser: urlUser)
+           
+        } else {
+            loadProfile(urlUser: urlUser)
+            loadTempBasals(urlUser: urlUser)
+            loadDeviceStatus(urlUser: urlUser)
+            updateMinAgo()
+            clearOldSnoozes()
+            checkAlarms(bgs: bgData)
+            loadProfile(urlUser: urlUser)
+        }
+        
+    }
+    
+    // Main NS BG Data Pull
+    func loadBGData(urlUser: String, onlyPullLastRecord: Bool = false) {
+
+        // Set the count= in the url either to pull 24 hours or only the last record
+        var points = "1"
+        if !onlyPullLastRecord {
+            points = String(self.graphHours * 12 + 1)
+        }
+
+        // URL processor
+        var urlBGDataPath: String = urlUser + "/api/v1/entries/sgv.json?"
+        if token == "" {
+            urlBGDataPath = urlBGDataPath + "count=" + points
+        } else {
+            urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
+        }
+        guard let urlBGData = URL(string: urlBGDataPath) else { return }
+        var request = URLRequest(url: urlBGData)
+        request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+
+        // Downloader
+        let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
+            if self.consoleLogging == true {print("start bg url")}
+            guard error == nil else {
+                return
+            }
+            guard let data = data else {
+                return
+            }
+
+            let decoder = JSONDecoder()
+            let entriesResponse = try? decoder.decode([sgvData].self, from: data)
+            if let entriesResponse = entriesResponse {
+                DispatchQueue.main.async {
+                    // trigger the processor for the data after downloading.
+                    self.ProcessNSBGData(data: entriesResponse, onlyPullLastRecord: onlyPullLastRecord)
+                }
+            } else {
+                return
+            }
+        }
+        getBGTask.resume()
+    }
+       
+    // Primary processor for what to do with the downloaded NS BG data
+    func ProcessNSBGData(data: [sgvData], onlyPullLastRecord: Bool){
+        var pullDate = data[data.count - 1].date / 1000
+        pullDate.round(FloatingPointRoundingRule.toNearestOrEven)
+        
+        // If we already have data, we're going to pop it to the end and remove the first. If we have old or no data, we'll destroy the whole array and start over. This is simpler than determining how far back we need to get new data from in case Dex back-filled readings
+        if !onlyPullLastRecord {
+            bgData.removeAll()
+        } else if bgData[bgData.count - 1].date != pullDate {
+            bgData.removeFirst()
+        } else {
+            // Update the badge, bg, graph settings even if we don't have a new reading.
+            self.updateBadge(entries: bgData)
+            self.updateBG(entries: bgData)
+            return
+        }
+        
+        // loop through the data so we can reverse the order to oldest first for the graph and convert the NS timestamp to seconds instead of milliseconds. Makes date comparisons easier for everything else.
+        for i in 0..<data.count{
+            var dateString = data[data.count - 1 - i].date / 1000
+            dateString.round(FloatingPointRoundingRule.toNearestOrEven)
+            let reading = sgvData(sgv: data[data.count - 1 - i].sgv, date: dateString, direction: data[data.count - 1 - i].direction)
+            bgData.append(reading)
+        }
+        self.updateBadge(entries: bgData)
+        self.updateBG(entries: bgData)
+        self.createGraph(entries: bgData)
+        if UserDefaultsRepository.writeCalendarEvent.value {
+            self.writeCalendar()
+        }
+       }
+    
+    func updateBG (entries: [sgvData]) {
+        if consoleLogging == true {print("in update BG")}
+        if entries.count > 0 {
+            let latestEntryi = entries.count - 1
+            let latestBG = entries[latestEntryi].sgv
+            let priorBG = entries[latestEntryi - 1].sgv
+            let deltaBG = latestBG - priorBG as Int
+            let lastBGTime = entries[latestEntryi].date //NS has different units
+            let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
+            var userUnit = " mg/dL"
+            if mmol {
+                userUnit = " mmol/L"
+            }
+            if UserDefaultsRepository.appBadge.value {
+                UIApplication.shared.applicationIconBadgeNumber = latestBG
+            } else {
+                UIApplication.shared.applicationIconBadgeNumber = 0
+            }
+            
+            BGText.text = bgOutputFormat(bg: Double(latestBG), mmol: mmol)
+            setBGTextColor()
+
+            MinAgoText.text = String(Int(deltaTime)) + " min ago"
+            print(String(Int(deltaTime)) + " min ago")
+            if let directionBG = entries[latestEntryi].direction {
+                DirectionText.text = bgDirectionGraphic(directionBG)
+            }
+            else
+            {
+                DirectionText.text = ""
+            }
+            
+          if deltaBG < 0 {
+            self.DeltaText.text = String(deltaBG)
+            }
+            else
+            {
+                self.DeltaText.text = "+" + String(deltaBG)
+            }
+        
+        }
+        else
+        {
+            
+            return
+        }
+        
+        checkAlarms(bgs: entries)
+    }
+    
+     // NS Device Status Pull from NS
+      func loadDeviceStatus(urlUser: String) {
+          var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=1"
+          if token != "" {
+              urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?token=" + token + "&count=1"
+          }
+          let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
+          guard let urlDeviceStatus = URL(string: escapedAddress!) else {
+              return
+          }
+          if consoleLogging == true {print("entered device status task.")}
+          var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
+          requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+          let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
+          if self.consoleLogging == true {print("in device status loop.")}
+          guard error == nil else {
+              return
+          }
+          guard let data = data else {
+              return
+          }
+              
+              let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
+          if let json = json {
+              DispatchQueue.main.async {
+                  self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
+              }
+          } else {
+              return
+          }
+          if self.consoleLogging == true {print("finish pump update")}}
+          deviceStatusTask.resume()
+      }
+      
+      // Parse Device Status Data
+      func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
+          if consoleLogging == true {print("in updatePump")}
+          if jsonDeviceStatus.count == 0 {
+              return
+          }
+          
+          //only grabbing one record since ns sorts by {created_at : -1}
+          let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
+          
+          //pump and uploader
+          let formatter = ISO8601DateFormatter()
+          formatter.formatOptions = [.withFullDate,
+                                     .withTime,
+                                     .withDashSeparatorInDate,
+                                     .withColonSeparatorInTime]
+          if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
+              if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970  {
+                  if let reservoirData = lastPumpRecord["reservoir"] as? Double
+                  {
+                      tableData[5].value = String(format:"%.0f", reservoirData) + "U"
+                  } else {
+                      tableData[5].value = "50+U"
+                  }
+                  
+                  if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
+                      let upbat = uploader["battery"] as! Double
+                      tableData[4].value = String(format:"%.0f", upbat) + "%"
+                  }
+              }
+          }
+              
+          // Loop
+          if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
+              if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970  {
+
+                  UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
+                  
+                  if let failure = lastLoopRecord["failureReason"] {
+                      LoopStatusLabel.text = "⚠"
+                  }
+                  else
+                  {
+                      if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
+                          if let lastTempBasal = enacted["rate"] as? Double {
+                              tableData[2].value = String(format:"%.1f", lastTempBasal)
+                          }
+                      }
+                      if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
+                          tableData[0].value = String(format:"%.1f", (iobdata["iob"] as! Double))
+                      }
+                      if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
+                          tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
+                      }
+                      if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
+                          let prediction = predictdata["values"] as! [Double]
+                          PredictionLabel.text = String(Int(prediction.last!))
+                          PredictionLabel.textColor = UIColor.systemPurple
+                          predictionData.removeAll()
+                          var i = 1
+                          while i <= 12 {
+                              predictionData.append(prediction[i])
+                              i += 1
+                          }
+                          
+                      }
+                      
+                      
+                      
+                      if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
+                          if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
+                              if tempBasalTime > lastLoopTime {
+                                  LoopStatusLabel.text = "⏀"
+                                 } else {
+                                  LoopStatusLabel.text = "↻"
+                              }
+                          }
+                         
+                      } else {
+                          LoopStatusLabel.text = "↻"
+                      }
+                      
+                  }
+                  if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 10 {
+                      LoopStatusLabel.text = "⚠"
+                  }
+              }
+              
+              
+              
+          }
+          
+          var oText = "" as String
+                 
+                 if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
+                     if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970  {
+                     }
+                     if lastOverride["active"] as! Bool {
+                         
+                         let lastCorrection  = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
+                         if let multiplier = lastOverride["multiplier"] as? Double {
+                                                oText += String(format:"%.1f", multiplier*100)
+                                            }
+                                            else
+                                            {
+                                                oText += String(format:"%.1f", 100)
+                                            }
+                         oText += "% ("
+                         let minValue = lastCorrection["minValue"] as! Double
+                         let maxValue = lastCorrection["maxValue"] as! Double
+                         oText += bgOutputFormat(bg: minValue, mmol: mmol) + "-" + bgOutputFormat(bg: maxValue, mmol: mmol) + ")"
+                        
+                      tableData[3].value =  oText
+                     }
+                 }
+          
+          infoTable.reloadData()
+          }
+      
+      // Get last CAGE entry
+      func loadCage(urlUser: String) {
+          var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Site%20Change&count=1"
+          if token != "" {
+              urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Site%20Change&count=1"
+          }
+
+          guard let urlData = URL(string: urlString) else {
+              return
+          }
+          var request = URLRequest(url: urlData)
+          request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+
+          let task = URLSession.shared.dataTask(with: request) { data, response, error in
+              if self.consoleLogging == true {print("start cage url")}
+              guard error == nil else {
+                  return
+              }
+              guard let data = data else {
+                  return
+              }
+
+              let decoder = JSONDecoder()
+              let entriesResponse = try? decoder.decode([cageData].self, from: data)
+              if let entriesResponse = entriesResponse {
+                  DispatchQueue.main.async {
+                      self.updateCage(data: entriesResponse)
+                  }
+              } else {
+                  return
+              }
+          }
+          task.resume()
+      }
+       
+      // Parse Cage Data
+      func updateCage(data: [cageData]) {
+          if consoleLogging == true {print("in updateCage")}
+          if data.count == 0 {
+              return
+          }
+
+          let lastCageString = data[0].created_at
+
+          let formatter = ISO8601DateFormatter()
+          formatter.formatOptions = [.withFullDate,
+                                  .withTime,
+                                  .withDashSeparatorInDate,
+                                  .withColonSeparatorInTime]
+          UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval
+          if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
+              let now = NSDate().timeIntervalSince1970
+              let secondsAgo = now - cageTime
+              //let days = 24 * 60 * 60
+
+              let formatter = DateComponentsFormatter()
+              formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
+              formatter.allowedUnits = [ .hour, .minute ] // Units to display in the formatted string
+              formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
+
+              let formattedDuration = formatter.string(from: secondsAgo)
+              tableData[7].value = formattedDuration ?? ""
+          }
+          infoTable.reloadData()
+      }
+       
+      // Get last SAGE entry
+      func loadSage(urlUser: String) {
+          var dayComponent    = DateComponents()
+          dayComponent.day    = -10 // For removing 10 days
+          let theCalendar     = Calendar.current
+
+          let startDate    = theCalendar.date(byAdding: dayComponent, to: Date())!
+          let dateFormatter = DateFormatter()
+          dateFormatter.dateFormat = "yyyy-MM-dd"
+          var startDateString = dateFormatter.string(from: startDate)
+
+
+          var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Sensor%20Start&find[created_at][$gte]=2020-05-31&count=1"
+          if token != "" {
+              urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Sensor%20Start&find[created_at][$gte]=2020-05-31&count=1"
+          }
+
+          guard let urlData = URL(string: urlString) else {
+              return
+          }
+          var request = URLRequest(url: urlData)
+          request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+
+          let task = URLSession.shared.dataTask(with: request) { data, response, error in
+              if self.consoleLogging == true {print("start cage url")}
+              guard error == nil else {
+                  return
+              }
+              guard let data = data else {
+                  return
+              }
+
+              let decoder = JSONDecoder()
+              let entriesResponse = try? decoder.decode([cageData].self, from: data)
+              if let entriesResponse = entriesResponse {
+                  DispatchQueue.main.async {
+                      self.updateSage(data: entriesResponse)
+                  }
+              } else {
+                  return
+              }
+          }
+          task.resume()
+      }
+       
+      // Parse Sage Data
+      func updateSage(data: [cageData]) {
+          if consoleLogging == true {print("in updateSage")}
+          if data.count == 0 {
+              return
+          }
+
+          var lastSageString = data[0].created_at
+
+          let formatter = ISO8601DateFormatter()
+          formatter.formatOptions = [.withFullDate,
+                               .withTime,
+                               .withDashSeparatorInDate,
+                               .withColonSeparatorInTime]
+          UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval
+          if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
+              let now = NSDate().timeIntervalSince1970
+              let secondsAgo = now - sageTime
+              let days = 24 * 60 * 60
+
+              let formatter = DateComponentsFormatter()
+              formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
+              formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
+              formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
+
+              let formattedDuration = formatter.string(from: secondsAgo)
+              tableData[6].value = formattedDuration ?? ""
+          }
+          infoTable.reloadData()
+      }
+       
+      // Load Current Profile
+      func loadProfile(urlUser: String) {
+          
+          let urlString = urlUser + "/api/v1/profile/current.json"
+          let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
+          guard let url = URL(string: escapedAddress!) else {
+              return
+          }
+          
+          var request = URLRequest(url: url)
+          request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+          let task = URLSession.shared.dataTask(with: request) { data, response, error in
+              guard error == nil else {
+                  return
+              }
+              guard let data = data else {
+                  return
+              }
+              
+              
+              let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
+              
+              if let json = json {
+                  DispatchQueue.main.async {
+                      self.updateProfile(jsonDeviceStatus: json)
+                  }
+              } else {
+                  return
+              }
+          }
+          task.resume()
+      }
+      
+      // Parse Basal schedule from the profile
+      func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
+     
+          if jsonDeviceStatus.count == 0 {
+              return
+          }
+          let basal = jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
+          for i in 0..<basal.count {
+              let dict = basal[i] as! Dictionary<String, Any>
+              let thisValue = dict[keyPath: "value"] as! Double
+              let thisTime = dict[keyPath: "time"] as! String
+              let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
+              let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
+              basalProfile.append(entry)
+          }
+          
+      }
+      
+
+      func loadTempBasals(urlUser: String) {
+          let today = Date()
+            let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
+            
+            let dateFormatter = DateFormatter()
+            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
+            dateFormatter.timeZone = TimeZone.init(secondsFromGMT: 0)
+            let yesterdayString = dateFormatter.string(from: yesterday)
+            
+            var urlString = urlUser + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
+            if token != "" {
+                urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
+            }
+            
+            guard let urlData = URL(string: urlString) else {
+                return
+            }
+            
+          
+            var request = URLRequest(url: urlData)
+            request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+            let task = URLSession.shared.dataTask(with: request) { data, response, error in
+           
+            guard error == nil else {
+                return
+            }
+            guard let data = data else {
+                return
+            }
+                
+                let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
+            if let json = json {
+                DispatchQueue.main.async {
+                    self.updateBasals(entries: json)
+                }
+            } else {
+                return
+            }
+            }
+            task.resume()
+      }
+      
+      func updateBasals(entries: [[String:AnyObject]]) {
+          
+          // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
+          basalData.removeAll()
+          
+        var lastEndDot = 0.0
+        
+          var tempArray: [basalGraphStruct] = []
+          for i in 0..<entries.count {
+              let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
+              let basalDate = currentEntry?["timestamp"] as! String
+              let strippedZone = String(basalDate.dropLast())
+              let dateFormatter = DateFormatter()
+              dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
+              dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
+              let dateString = dateFormatter.date(from: strippedZone)
+              let dateTimeStamp = dateString!.timeIntervalSince1970
+              let basalRate = currentEntry?["absolute"] as! Double
+                let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
+              // Setting end dots
+              var duration = 0.0
+              // For all except the last we're going to use stored duration for end dot. For last we'll just put it 5 mintues after the entry
+              if i <= entries.count - 1 {
+                   duration = currentEntry?["duration"] as! Double
+              } else {
+                  duration = dateTimeStamp + 60
+              }
+              
+              // This adds scheduled basal wherever there is a break between temps. can't check the prior ending on the first item. it is 24 hours old, so it isn't important for display anyway
+              if i > 0 {
+                  let priorEntry = entries[entries.count - i] as [String : AnyObject]?
+                  let priorBasalDate = priorEntry?["timestamp"] as! String
+                  let priorStrippedZone = String(priorBasalDate.dropLast())
+                  let priorDateFormatter = DateFormatter()
+                  priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
+                  priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
+                  let priorDateString = dateFormatter.date(from: priorStrippedZone)
+                  let priorDateTimeStamp = priorDateString!.timeIntervalSince1970
+                 let priorDuration = priorEntry?["duration"] as! Double
+                // if difference between time stamps is greater than the duration of the last entry, there is a gap
+                if Double( dateTimeStamp - priorDateTimeStamp ) > Double( priorDuration * 60 ) {
+                     
+                      var scheduled = 0.0
+                    // cycle through basal profiles.
+                    // TODO figure out how to deal with profile changes that happen mid-gap
+                      for b in 0..<self.basalProfile.count {
+                        let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
+                        let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
+                        // check the prior temp ending to the profile seconds from midnight
+                        if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeYesterday {
+                            scheduled = basalProfile[b].value
+                        }
+                        if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeToday {
+                            scheduled = basalProfile[b].value
+                        }
+                          
+                        // This will iterate through from midnight on and set it for the highest matching one.
+                      }
+                      // Make the starting dot at the last ending dot
+                      let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
+                      basalData.append(startDot)
+                    
+                    // Make the ending dot at the new starting dot
+                    let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
+                    basalData.append(endDot)
+                      
+                     
+                 }
+              }
+              
+              // Make the starting dot
+              let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
+              basalData.append(startDot)
+            
+            // Make the ending dot
+            // If it's the last one and not ended yet, extend it for 1 hour to matc the prediction length. Otherwise let it end
+            if i == entries.count - 1 && dateTimeStamp + duration <= Date().timeIntervalSince1970 {
+                lastEndDot = Date().timeIntervalSince1970 + (60 * 60)
+            } else {
+                lastEndDot = dateTimeStamp + (duration * 60)
+            }
+            let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
+            basalData.append(endDot)
+            
+              
+          }
+        
+        // If last scheduled basal was prior to right now, we need to create one last scheduled entry
+        if lastEndDot <= Date().timeIntervalSince1970 {
+            var scheduled = 0.0
+                // cycle through basal profiles.
+                // TODO figure out how to deal with profile changes that happen mid-gap
+                  for b in 0..<self.basalProfile.count {
+                    let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
+                    let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
+                    // check the prior temp ending to the profile seconds from midnight
+                    if lastEndDot >= scheduleTimeYesterday {
+                        scheduled = basalProfile[b].value
+                    }
+                    if lastEndDot >= scheduleTimeToday {
+                        scheduled = basalProfile[b].value
+                    }
+                      
+                    
+                  }
+                  // Make the starting dot at the last ending dot
+                  let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
+                  basalData.append(startDot)
+                
+                // Make the ending dot 1 hour after now
+                let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970))
+                basalData.append(endDot)
+                  
+                 
+             }
+            
+        
+        
+        /*
+        // Add 1 hour to end to match prediction time length
+        var scheduled = 0.0
+        // cycle through basal profiles to get current schedule for end.
+        // TODO figure out how to deal with profile changes that happen mid-gap
+        
+          for b in 0..<self.basalProfile.count {
+            let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
+            // check the last end time to the profile seconds from midnight
+            if lastEndDot >= scheduleTimeToday {
+                scheduled = basalProfile[b].value
+            }
+          }
+          // Make the starting dot at the last ending dot
+          let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
+          basalData.append(startDot)
+        
+        // Make the ending dot 1 hour from the latest BG reading
+        let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970 + (60 * 60)))
+        basalData.append(endDot)
+        */
+        createBasalGraph(entries: basalData)
+      }
+    
+      // NOT IMPLEMENTED YET
+      func loadBoluses(urlUser: String){
+          var calendar = Calendar.current
+          let today = Date()
+          let midnight = calendar.startOfDay(for: today)
+          let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+          let dateFormatter = DateFormatter()
+          dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
+          dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
+          let formattedDate = dateFormatter.string(from: yesterday)
+          
+          var searchString = "find[eventType]=Meal%20Bolus&find[created_at][$gte]=" + formattedDate
+          var urlBGDataPath: String = urlUser + "/api/v1/treatments.json?"
+          if token == "" {
+              urlBGDataPath = urlBGDataPath + searchString
+          }
+          else
+          {
+              urlBGDataPath = urlBGDataPath + "token=" + token + searchString
+          }
+          guard let urlBGData = URL(string: urlBGDataPath) else {
+              return
+          }
+          var request = URLRequest(url: urlBGData)
+          request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+          
+          let getTask = URLSession.shared.dataTask(with: request) { data, response, error in
+              if self.consoleLogging == true {print("start meal bolus url")}
+              guard error == nil else {
+                  return
+              }
+              guard let data = data else {
+                  return
+              }
+              
+              let decoder = JSONDecoder()
+              let entriesResponse = try? decoder.decode([sgvData].self, from: data)
+              if let entriesResponse = entriesResponse {
+                  DispatchQueue.main.async {
+                     // self.ProcessNSData(data: entriesResponse, onlyPullLastRecord: onlyPullLastRecord)
+                  }
+              }
+              else
+              {
+                  
+                  return
+              }
+          }
+          getTask.resume()
+      }
+    
+    
+}

+ 1 - 4
LoopFollow/LoopFollow.xcdatamodeld/.xccurrentversion

@@ -1,8 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
-<dict>
-	<key>_XCCurrentVersionName</key>
-	<string>LoopFollow.xcdatamodel</string>
-</dict>
+<dict/>
 </plist>

LoopFollow/Alarm Sounds/1-millisecond-of-silence.mp3 → LoopFollow/Resources/Alarm Sounds/1-millisecond-of-silence.mp3


LoopFollow/Alarm Sounds/20ms-of-silence.caf → LoopFollow/Resources/Alarm Sounds/20ms-of-silence.caf


LoopFollow/Alarm Sounds/500ms-of-silence.mp3 → LoopFollow/Resources/Alarm Sounds/500ms-of-silence.mp3


LoopFollow/Alarm Sounds/Alarm_Buzzer.caf → LoopFollow/Resources/Alarm Sounds/Alarm_Buzzer.caf


LoopFollow/Alarm Sounds/Alarm_Clock.caf → LoopFollow/Resources/Alarm Sounds/Alarm_Clock.caf


LoopFollow/Alarm Sounds/Alert_Tone_Busy.caf → LoopFollow/Resources/Alarm Sounds/Alert_Tone_Busy.caf


LoopFollow/Alarm Sounds/Alert_Tone_Ringtone_1.caf → LoopFollow/Resources/Alarm Sounds/Alert_Tone_Ringtone_1.caf


LoopFollow/Alarm Sounds/Alert_Tone_Ringtone_2.caf → LoopFollow/Resources/Alarm Sounds/Alert_Tone_Ringtone_2.caf


LoopFollow/Alarm Sounds/Alien_Siren.caf → LoopFollow/Resources/Alarm Sounds/Alien_Siren.caf


LoopFollow/Alarm Sounds/Ambulance.caf → LoopFollow/Resources/Alarm Sounds/Ambulance.caf


LoopFollow/Alarm Sounds/Analog_Watch_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Analog_Watch_Alarm.caf


LoopFollow/Alarm Sounds/Big_Clock_Ticking.caf → LoopFollow/Resources/Alarm Sounds/Big_Clock_Ticking.caf


LoopFollow/Alarm Sounds/Burglar_Alarm_Siren_1.caf → LoopFollow/Resources/Alarm Sounds/Burglar_Alarm_Siren_1.caf


LoopFollow/Alarm Sounds/Burglar_Alarm_Siren_2.caf → LoopFollow/Resources/Alarm Sounds/Burglar_Alarm_Siren_2.caf


LoopFollow/Alarm Sounds/CSFX-2_Alarm.caf → LoopFollow/Resources/Alarm Sounds/CSFX-2_Alarm.caf


LoopFollow/Alarm Sounds/Cartoon_Ascend_Climb_Sneaky.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Ascend_Climb_Sneaky.caf


LoopFollow/Alarm Sounds/Cartoon_Ascend_Then_Descend.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Ascend_Then_Descend.caf


LoopFollow/Alarm Sounds/Cartoon_Bounce_To_Ceiling.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Bounce_To_Ceiling.caf


LoopFollow/Alarm Sounds/Cartoon_Dreamy_Glissando_Harp.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Dreamy_Glissando_Harp.caf


LoopFollow/Alarm Sounds/Cartoon_Fail_Strings_Trumpet.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Fail_Strings_Trumpet.caf


LoopFollow/Alarm Sounds/Cartoon_Machine_Clumsy_Loop.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Machine_Clumsy_Loop.caf


LoopFollow/Alarm Sounds/Cartoon_Siren.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Siren.caf


LoopFollow/Alarm Sounds/Cartoon_Tip_Toe_Sneaky_Walk.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Tip_Toe_Sneaky_Walk.caf


LoopFollow/Alarm Sounds/Cartoon_Uh_Oh.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Uh_Oh.caf


LoopFollow/Alarm Sounds/Cartoon_Villain_Horns.caf → LoopFollow/Resources/Alarm Sounds/Cartoon_Villain_Horns.caf


LoopFollow/Alarm Sounds/Cell_Phone_Ring_Tone.caf → LoopFollow/Resources/Alarm Sounds/Cell_Phone_Ring_Tone.caf


LoopFollow/Alarm Sounds/Chimes_Glassy.caf → LoopFollow/Resources/Alarm Sounds/Chimes_Glassy.caf


LoopFollow/Alarm Sounds/Computer_Magic.caf → LoopFollow/Resources/Alarm Sounds/Computer_Magic.caf


LoopFollow/Alarm Sounds/Cuckoo_Clock.caf → LoopFollow/Resources/Alarm Sounds/Cuckoo_Clock.caf


LoopFollow/Alarm Sounds/Dhol_Shuffleloop.caf → LoopFollow/Resources/Alarm Sounds/Dhol_Shuffleloop.caf


LoopFollow/Alarm Sounds/Discreet.caf → LoopFollow/Resources/Alarm Sounds/Discreet.caf


LoopFollow/Alarm Sounds/Early_Sunrise.caf → LoopFollow/Resources/Alarm Sounds/Early_Sunrise.caf


LoopFollow/Alarm Sounds/Emergency_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Emergency_Alarm.caf


LoopFollow/Alarm Sounds/Emergency_Alarm_Carbon_Monoxide.caf → LoopFollow/Resources/Alarm Sounds/Emergency_Alarm_Carbon_Monoxide.caf


LoopFollow/Alarm Sounds/Emergency_Alarm_Siren.caf → LoopFollow/Resources/Alarm Sounds/Emergency_Alarm_Siren.caf


LoopFollow/Alarm Sounds/Ending_Reached.caf → LoopFollow/Resources/Alarm Sounds/Ending_Reached.caf


LoopFollow/Alarm Sounds/Fly.caf → LoopFollow/Resources/Alarm Sounds/Fly.caf


LoopFollow/Alarm Sounds/Ghost_Hover.caf → LoopFollow/Resources/Alarm Sounds/Ghost_Hover.caf


LoopFollow/Alarm Sounds/Good_Morning.caf → LoopFollow/Resources/Alarm Sounds/Good_Morning.caf


LoopFollow/Alarm Sounds/Hell_Yeah_Somewhat_Calmer.caf → LoopFollow/Resources/Alarm Sounds/Hell_Yeah_Somewhat_Calmer.caf


LoopFollow/Alarm Sounds/In_A_Hurry.caf → LoopFollow/Resources/Alarm Sounds/In_A_Hurry.caf


LoopFollow/Alarm Sounds/Indeed.caf → LoopFollow/Resources/Alarm Sounds/Indeed.caf


LoopFollow/Alarm Sounds/Insistently.caf → LoopFollow/Resources/Alarm Sounds/Insistently.caf


LoopFollow/Alarm Sounds/Jingle_All_The_Way.caf → LoopFollow/Resources/Alarm Sounds/Jingle_All_The_Way.caf


LoopFollow/Alarm Sounds/Laser_Shoot.caf → LoopFollow/Resources/Alarm Sounds/Laser_Shoot.caf


LoopFollow/Alarm Sounds/Machine_Charge.caf → LoopFollow/Resources/Alarm Sounds/Machine_Charge.caf


LoopFollow/Alarm Sounds/Magical_Twinkle.caf → LoopFollow/Resources/Alarm Sounds/Magical_Twinkle.caf


LoopFollow/Alarm Sounds/Marching_Heavy_Footed_Fat_Elephants.caf → LoopFollow/Resources/Alarm Sounds/Marching_Heavy_Footed_Fat_Elephants.caf


LoopFollow/Alarm Sounds/Marimba_Descend.caf → LoopFollow/Resources/Alarm Sounds/Marimba_Descend.caf


LoopFollow/Alarm Sounds/Marimba_Flutter_or_Shake.caf → LoopFollow/Resources/Alarm Sounds/Marimba_Flutter_or_Shake.caf


LoopFollow/Alarm Sounds/Martian_Gun.caf → LoopFollow/Resources/Alarm Sounds/Martian_Gun.caf


LoopFollow/Alarm Sounds/Martian_Scanner.caf → LoopFollow/Resources/Alarm Sounds/Martian_Scanner.caf


LoopFollow/Alarm Sounds/Metallic.caf → LoopFollow/Resources/Alarm Sounds/Metallic.caf


LoopFollow/Alarm Sounds/Nightguard.caf → LoopFollow/Resources/Alarm Sounds/Nightguard.caf


LoopFollow/Alarm Sounds/Not_Kiddin.caf → LoopFollow/Resources/Alarm Sounds/Not_Kiddin.caf


LoopFollow/Alarm Sounds/Open_Your_Eyes_And_See.caf → LoopFollow/Resources/Alarm Sounds/Open_Your_Eyes_And_See.caf


LoopFollow/Alarm Sounds/Orchestral_Horns.caf → LoopFollow/Resources/Alarm Sounds/Orchestral_Horns.caf


LoopFollow/Alarm Sounds/Oringz.caf → LoopFollow/Resources/Alarm Sounds/Oringz.caf


LoopFollow/Alarm Sounds/Pager_Beeps.caf → LoopFollow/Resources/Alarm Sounds/Pager_Beeps.caf


LoopFollow/Alarm Sounds/Remembers_Me_Of_Asia.caf → LoopFollow/Resources/Alarm Sounds/Remembers_Me_Of_Asia.caf


LoopFollow/Alarm Sounds/Rise_And_Shine.caf → LoopFollow/Resources/Alarm Sounds/Rise_And_Shine.caf


LoopFollow/Alarm Sounds/Rush.caf → LoopFollow/Resources/Alarm Sounds/Rush.caf


LoopFollow/Alarm Sounds/Sci-Fi_Air_Raid_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Air_Raid_Alarm.caf


LoopFollow/Alarm Sounds/Sci-Fi_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm.caf


LoopFollow/Alarm Sounds/Sci-Fi_Alarm_Loop_1.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_1.caf


LoopFollow/Alarm Sounds/Sci-Fi_Alarm_Loop_2.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_2.caf


LoopFollow/Alarm Sounds/Sci-Fi_Alarm_Loop_3.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_3.caf


LoopFollow/Alarm Sounds/Sci-Fi_Alarm_Loop_4.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Alarm_Loop_4.caf


LoopFollow/Alarm Sounds/Sci-Fi_Computer_Console_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Computer_Console_Alarm.caf


LoopFollow/Alarm Sounds/Sci-Fi_Console_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Console_Alarm.caf


LoopFollow/Alarm Sounds/Sci-Fi_Eerie_Alarm.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Eerie_Alarm.caf


LoopFollow/Alarm Sounds/Sci-Fi_Engine_Shut_Down.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Engine_Shut_Down.caf


LoopFollow/Alarm Sounds/Sci-Fi_Incoming_Message_Alert.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Incoming_Message_Alert.caf


LoopFollow/Alarm Sounds/Sci-Fi_Spaceship_Message.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Spaceship_Message.caf


LoopFollow/Alarm Sounds/Sci-Fi_Spaceship_Warm_Up.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Spaceship_Warm_Up.caf


LoopFollow/Alarm Sounds/Sci-Fi_Warning.caf → LoopFollow/Resources/Alarm Sounds/Sci-Fi_Warning.caf


LoopFollow/Alarm Sounds/Signature_Corporate.caf → LoopFollow/Resources/Alarm Sounds/Signature_Corporate.caf


LoopFollow/Alarm Sounds/Siri_Alert_Calibration_Needed.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Calibration_Needed.caf


LoopFollow/Alarm Sounds/Siri_Alert_Device_Muted.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Device_Muted.caf


LoopFollow/Alarm Sounds/Siri_Alert_Glucose_Dropping_Fast.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Glucose_Dropping_Fast.caf


LoopFollow/Alarm Sounds/Siri_Alert_Glucose_Rising_Fast.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Glucose_Rising_Fast.caf


LoopFollow/Alarm Sounds/Siri_Alert_High_Glucose.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_High_Glucose.caf


LoopFollow/Alarm Sounds/Siri_Alert_Low_Glucose.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Low_Glucose.caf


LoopFollow/Alarm Sounds/Siri_Alert_Missed_Readings.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Missed_Readings.caf


LoopFollow/Alarm Sounds/Siri_Alert_Transmitter_Battery_Low.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Transmitter_Battery_Low.caf


LoopFollow/Alarm Sounds/Siri_Alert_Urgent_High_Glucose.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Urgent_High_Glucose.caf


LoopFollow/Alarm Sounds/Siri_Alert_Urgent_Low_Glucose.caf → LoopFollow/Resources/Alarm Sounds/Siri_Alert_Urgent_Low_Glucose.caf


LoopFollow/Alarm Sounds/Siri_Calibration_Needed.caf → LoopFollow/Resources/Alarm Sounds/Siri_Calibration_Needed.caf


+ 0 - 0
LoopFollow/Alarm Sounds/Siri_Device_Muted.caf


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác