Browse Source

Added DND, Volume, Vibrate, Fadein alarm settings
Added prediction BG and Loop status under main BG display

Jon Fawcett 6 năm trước cách đây
mục cha
commit
b8b73ef594

+ 48 - 23
LoopFollow/Base.lproj/Main.storyboard

@@ -4,6 +4,7 @@
     <dependencies>
         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="Stack View standard spacing" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -16,46 +17,68 @@
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <subviews>
                             <stackView opaque="NO" contentMode="scaleToFill" spacing="54" translatesAutoresizingMaskIntoConstraints="NO" id="mJy-Ly-Jze">
-                                <rect key="frame" x="8" y="52" width="398" height="250"/>
+                                <rect key="frame" x="8" y="52" width="398" height="336"/>
                                 <subviews>
                                     <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Ptf-fu-Ue4" userLabel="BGview">
-                                        <rect key="frame" x="0.0" y="0.0" width="121" height="250"/>
+                                        <rect key="frame" x="0.0" y="0.0" width="127.33333333333333" height="336"/>
                                         <subviews>
                                             <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="BG" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vXU-IW-GOL">
-                                                <rect key="frame" x="0.0" y="0.0" width="121" height="125.66666666666667"/>
+                                                <rect key="frame" x="0.0" y="0.0" width="127.33333333333333" height="223.66666666666666"/>
                                                 <fontDescription key="fontDescription" type="system" weight="black" pointSize="85"/>
                                                 <nil key="textColor"/>
                                                 <nil key="highlightedColor"/>
                                             </label>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="--" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4sd-6h-LSj" userLabel="Direction">
-                                                <rect key="frame" x="0.0" y="125.66666666666666" width="121" height="71.666666666666657"/>
-                                                <fontDescription key="fontDescription" type="system" weight="black" pointSize="60"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Delta" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sb7-g9-JNv" userLabel="Delta">
-                                                <rect key="frame" x="0.0" y="197.33333333333334" width="121" height="38.333333333333343"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="32"/>
-                                                <nil key="textColor"/>
-                                                <color key="highlightedColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                            </label>
+                                            <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KZc-fa-qmt">
+                                                <rect key="frame" x="0.0" y="223.66666666666666" width="127.33333333333333" height="71.666666666666657"/>
+                                                <subviews>
+                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="--" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4sd-6h-LSj" userLabel="Direction">
+                                                        <rect key="frame" x="0.0" y="0.0" width="55.333333333333336" height="71.666666666666671"/>
+                                                        <fontDescription key="fontDescription" type="system" weight="black" pointSize="60"/>
+                                                        <nil key="textColor"/>
+                                                        <nil key="highlightedColor"/>
+                                                    </label>
+                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Delta" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sb7-g9-JNv" userLabel="Delta">
+                                                        <rect key="frame" x="55.333333333333343" y="0.0" width="72" height="71.666666666666671"/>
+                                                        <fontDescription key="fontDescription" type="system" pointSize="32"/>
+                                                        <nil key="textColor"/>
+                                                        <color key="highlightedColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                                    </label>
+                                                </subviews>
+                                            </stackView>
                                             <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="MinAgo" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UkG-hw-DVo">
-                                                <rect key="frame" x="0.0" y="235.66666666666669" width="121" height="14.333333333333343"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="12"/>
+                                                <rect key="frame" x="0.0" y="295.33333333333331" width="127.33333333333333" height="20.333333333333314"/>
+                                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                                 <nil key="textColor"/>
                                                 <nil key="highlightedColor"/>
                                             </label>
+                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="TDG-yw-5fS">
+                                                <rect key="frame" x="0.0" y="315.66666666666669" width="127.33333333333333" height="20.333333333333314"/>
+                                                <subviews>
+                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loop" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XEm-uK-Xui">
+                                                        <rect key="frame" x="0.0" y="0.0" width="59.666666666666664" height="20.333333333333332"/>
+                                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                                        <nil key="textColor"/>
+                                                        <nil key="highlightedColor"/>
+                                                    </label>
+                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Prediction" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oiR-SM-6sg">
+                                                        <rect key="frame" x="67.666666666666671" y="0.0" width="59.666666666666671" height="20.333333333333332"/>
+                                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                                        <nil key="textColor"/>
+                                                        <nil key="highlightedColor"/>
+                                                    </label>
+                                                </subviews>
+                                            </stackView>
                                         </subviews>
                                     </stackView>
                                     <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="kTs-7g-lp9">
-                                        <rect key="frame" x="175" y="0.0" width="223" height="250"/>
+                                        <rect key="frame" x="181.33333333333337" y="0.0" width="216.66666666666663" height="336"/>
                                         <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
                                         <prototypes>
                                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="LabelCell" textLabel="gif-qe-9mv" detailTextLabel="hfT-xF-eNt" style="IBUITableViewCellStyleValue1" id="GJo-f1-ajA">
-                                                <rect key="frame" x="0.0" y="28" width="223" height="43.666667938232422"/>
+                                                <rect key="frame" x="0.0" y="28" width="216.66666666666663" height="43.666667938232422"/>
                                                 <autoresizingMask key="autoresizingMask"/>
                                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GJo-f1-ajA" id="2H1-eI-xS5">
-                                                    <rect key="frame" x="0.0" y="0.0" width="223" height="43.666667938232422"/>
+                                                    <rect key="frame" x="0.0" y="0.0" width="216.66666666666663" height="43.666667938232422"/>
                                                     <autoresizingMask key="autoresizingMask"/>
                                                     <subviews>
                                                         <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="gif-qe-9mv">
@@ -65,8 +88,8 @@
                                                             <nil key="textColor"/>
                                                             <nil key="highlightedColor"/>
                                                         </label>
-                                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="characterWrap" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" id="hfT-xF-eNt">
-                                                            <rect key="frame" x="163" y="11.999999999999998" width="44" height="20.333333333333332"/>
+                                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="characterWrap" baselineAdjustment="none" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" id="hfT-xF-eNt">
+                                                            <rect key="frame" x="156.66666666666663" y="11.999999999999998" width="44" height="20.333333333333332"/>
                                                             <autoresizingMask key="autoresizingMask"/>
                                                             <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                                             <nil key="textColor"/>
@@ -80,7 +103,7 @@
                                 </subviews>
                             </stackView>
                             <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="51B-JU-npv" customClass="LineChartView" customModule="Charts">
-                                <rect key="frame" x="8" y="310" width="398" height="427"/>
+                                <rect key="frame" x="8" y="396" width="398" height="341"/>
                                 <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
                             </view>
                             <view autoresizesSubviews="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nml-lr-dgQ" userLabel="BGChartFull" customClass="LineChartView" customModule="Charts">
@@ -113,7 +136,9 @@
                         <outlet property="BGText" destination="vXU-IW-GOL" id="2aC-pv-Qbg"/>
                         <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"/>
                         <outlet property="MinAgoText" destination="UkG-hw-DVo" id="Xd2-fL-qYC"/>
+                        <outlet property="PredictionLabel" destination="oiR-SM-6sg" id="2Sw-TR-SFo"/>
                         <outlet property="infoTable" destination="kTs-7g-lp9" id="Khq-sV-EZ1"/>
                     </connections>
                 </viewController>

+ 277 - 282
LoopFollow/Controllers/MainViewController.swift

@@ -10,7 +10,7 @@ import UIKit
 import Charts
 
 
-class MainViewController: UIViewController, UITableViewDataSource {
+class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate {
     
     @IBOutlet weak var BGText: UILabel!
     @IBOutlet weak var DeltaText: UILabel!
@@ -21,6 +21,8 @@ class MainViewController: UIViewController, UITableViewDataSource {
     @IBOutlet weak var infoTable: UITableView!
     @IBOutlet weak var Console: UITableViewCell!
     @IBOutlet weak var DragBar: UIImageView!
+    @IBOutlet weak var PredictionLabel: UILabel!
+    @IBOutlet weak var LoopStatusLabel: UILabel!
     
     //NS BG Struct
     struct sgvData: Codable {
@@ -40,7 +42,6 @@ class MainViewController: UIViewController, UITableViewDataSource {
         var value: String
     }
 
-    
     // Variables for BG Charts
     public var numPoints: Int = 13
     public var linePlotData: [Double] = []
@@ -76,9 +77,8 @@ class MainViewController: UIViewController, UITableViewDataSource {
         infoData(name: "Override", value: ""), //3
         infoData(name: "Battery", value: ""), //4
         infoData(name: "Pump", value: ""), //5
-        infoData(name: "Loop", value: ""), //6
-        infoData(name: "SAGE", value: ""), //7
-        infoData(name: "CAGE", value: "") //8
+        infoData(name: "SAGE", value: ""), //6
+        infoData(name: "CAGE", value: "") //7
     ]
     
     var bgData: [sgvData] = []
@@ -88,20 +88,20 @@ class MainViewController: UIViewController, UITableViewDataSource {
     override func viewDidLoad() {
         super.viewDidLoad()
         
+        // Disable the snoozer tab unless an alarm is active
         let tabBarControllerItems = self.tabBarController?.tabBar.items
         if let arrayOfTabBarItems = tabBarControllerItems as! AnyObject as? NSArray{
             snoozeTabItem = arrayOfTabBarItems[2] as! UITabBarItem
         }
         snoozeTabItem.isEnabled = false;
         
+        // Grant Badge Access
         UNUserNotificationCenter.current().requestAuthorization(options: .badge) { (granted, error) in
             if error != nil {
-                // success!
             }
         }
         
-        // ToDo: Should continue running in background
-        // stop timer when app enters in background, start is again when becomes active
+        // Trigger foreground and background functions
         let notificationCenter = NotificationCenter.default
             notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
             notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
@@ -113,6 +113,7 @@ class MainViewController: UIViewController, UITableViewDataSource {
     }
     
     override func viewWillAppear(_ animated: Bool) {
+        // Pull fresh data when view appears
         nightscoutLoader()
     }
     
@@ -130,7 +131,7 @@ class MainViewController: UIViewController, UITableViewDataSource {
         return cell
     }
     
-    // Timer
+    // NS Loader Timer
     fileprivate func startTimer(time: TimeInterval) {
         timer = Timer.scheduledTimer(timeInterval: time,
                                      target: self,
@@ -139,7 +140,7 @@ class MainViewController: UIViewController, UITableViewDataSource {
                                      repeats: true)
     }
     
-    // Timer
+    // Check Alarm Timer
     func startCheckAlarmTimer(time: TimeInterval) {
         checkAlarmTimer = Timer.scheduledTimer(timeInterval: time,
                                      target: self,
@@ -148,13 +149,16 @@ class MainViewController: UIViewController, UITableViewDataSource {
                                      repeats: false)
     }
     
-    // check whether new Values should be retrieved
+    // Nothing should be done when this timer ends because it just blocks the alarms from firing when it's active
     @objc func checkAlarmTimerDidEnd(_ timer:Timer) {
         print("check alarm timer ended")
     }
     
     @objc func appMovedToBackground() {
+        // We want to always come back to the home screen
         tabBarController?.selectedIndex = 0
+        
+        // Cancel the current timer and start a fresh background timer using the settings value only if background task is enabled
         timer.invalidate()
         if UserDefaultsRepository.backgroundRefresh.value {
             backgroundTask.startBackgroundTask()
@@ -162,26 +166,30 @@ class MainViewController: UIViewController, UITableViewDataSource {
         }
     }
 
-   @objc func appCameToForeground() {
-       if UserDefaultsRepository.backgroundRefresh.value {
+    @objc func appCameToForeground() {
+        // Cancel the background tasks, start a fresh timer, and immediately check for new data
+        if UserDefaultsRepository.backgroundRefresh.value {
             backgroundTask.stopBackgroundTask()
             timer.invalidate()
         }
         startTimer(time: timeInterval)
         nightscoutLoader()
+
+    }
     
-   }
-    
-    // check whether new Values should be retrieved
+    // Check for new data when timer ends
     @objc func timerDidEnd(_ timer:Timer) {
         print("timer ended")
         nightscoutLoader()
     }
     
+    // Main loader for all data
     func nightscoutLoader() {
         
         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
@@ -195,6 +203,8 @@ class MainViewController: UIViewController, UITableViewDataSource {
         } else {
             needsLoaded = true
         }
+        
+        // Only do the network calls if we don't have a current reading
         if needsLoaded {
             loadBGData(urlUser: urlUser, onlyPullLastRecord: onlyPullLastRecord)
             clearLastInfoData()
@@ -204,6 +214,7 @@ class MainViewController: UIViewController, UITableViewDataSource {
            // loadBoluses(urlUser: urlUser)
            // loadTempBasals(urlUser: urlUser)
         } else {
+            // We're still going to process the Min Ago display and alarm sections without a network call. A snoozer could have expired during since the last call
             updateMinAgo()
             clearOldSnoozes()
             checkAlarms(bgs: bgData)
@@ -211,61 +222,27 @@ class MainViewController: UIViewController, UITableViewDataSource {
         
     }
     
-    // Post process new NS Data and feed all updates
-    func ProcessNSBGData(data: [sgvData], onlyPullLastRecord: Bool){
-        var pullDate = data[data.count - 1].date / 1000
-        pullDate.round(FloatingPointRoundingRule.toNearestOrEven)
-        if !onlyPullLastRecord {
-            bgData.removeAll()
-        } else if bgData[bgData.count - 1].date != pullDate {
-            bgData.removeFirst()
-        } else {
-            self.updateBadge(entries: bgData)
-            self.updateBG(entries: bgData)
-            self.createGraph(entries: bgData)
-            return
-        }
-
-        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)
-       }
-    
-    
-    //update Min Ago
-    func updateMinAgo(){
-        let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-bgData[bgData.count - 1].date) / 60
-        MinAgoText.text = String(Int(deltaTime)) + " min ago"
-    }
-
-    // Main NS Data Pull
+    // 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)
+            points = String(self.graphHours * 12 + 1)
         }
-        
-        
+
+        // URL processor
         var urlBGDataPath: String = urlUser + "/api/v1/entries/sgv.json?"
         if token == "" {
             urlBGDataPath = urlBGDataPath + "count=" + points
-        }
-        else
-        {
+        } else {
             urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
         }
-        guard let urlBGData = URL(string: urlBGDataPath) else {
-            return
-        }
+        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 {
@@ -274,125 +251,203 @@ class MainViewController: UIViewController, UITableViewDataSource {
             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
-            {
-                
+            } 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)
+            self.createGraph(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)
+       }
     
+    //update Min Ago Text. We need to call this separately because it updates between readings
+    func updateMinAgo(){
+        let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-bgData[bgData.count - 1].date) / 60
+        MinAgoText.text = String(Int(deltaTime)) + " min ago"
+    }
     
-    //Clear the info data before next pull
+    //Clear the info data before next pull. This ensures we aren't displaying old data if something fails.
     func clearLastInfoData(){
-       for i in 0..<tableData.count{
-        tableData[i].value = ""
+        for i in 0..<tableData.count{
+            tableData[i].value = ""
         }
     }
     
-    // NS Device Status Pull
+    // 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 update loop.")}
-            guard error == nil else {
-                return
-            }
-            guard let data = data else {
-                
-                return
-            }
-            
-            let json = try? JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]]
+        if self.consoleLogging == true {print("in device status loop.")}
+        guard error == nil else {
+            return
+        }
+        guard let data = data else {
+            return
+        }
             
-            if let json = json {
-                DispatchQueue.main.async {
-                    self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
-                }
+        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")}
+        } else {
+            return
         }
+        if self.consoleLogging == true {print("finish pump update")}}
         deviceStatusTask.resume()
     }
     
-    // Need to figure out the date to pull only last 24 hours
-    func loadTempBasals(urlUser: String) {
-        var dayComponent    = DateComponents()
-        dayComponent.day    = -1 // For removing one day (yesterday): -1
-        let theCalendar     = Calendar.current
-    
-        let yesterday       = theCalendar.date(byAdding: dayComponent, to: Date())
-        let dateFormatter = DateFormatter()
-        dateFormatter.dateFormat = "yyyy-mm-ddTHH:mm:ss"
-        //dateFormatter.timeZone = NSTimeZone(name: "UTC")
-        //let date: NSDate? = dateFormatter.dateFromString("2016-02-29 12:24:26")
-        //print(date)
-        
-        var urlStringBasal = urlUser + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=2020-06-02T22:46:26"
-        if token != "" {
-            urlStringBasal = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=2020-06-02T22:46:26"
-        }
-        
-        let escapedAddress = urlStringBasal.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
-        guard let urlBasal = URL(string: escapedAddress!) else {
+    // Parse Device Status Data
+    func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
+        if consoleLogging == true {print("in updatePump")}
+        if jsonDeviceStatus.count == 0 {
             return
         }
         
-        if consoleLogging == true {print("entered 2nd task.")}
-        var requestBasal = URLRequest(url: urlBasal)
-        requestBasal.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
-        let basalTask = URLSession.shared.dataTask(with: requestBasal) { data, response, error in
-            if self.consoleLogging == true {print("in update loop.")}
-            guard error == nil else {
-                return
-            }
-            guard let data = data else {
+        //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"
+                }
                 
-                return
+                if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
+                    let upbat = uploader["battery"] as! Double
+                    tableData[4].value = String(format:"%.0f", upbat) + "%"
+                }
             }
+        }
             
-            let json = try? JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]]
-            
-            if let json = json {
-                DispatchQueue.main.async {
-                    self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
+        // Loop
+        if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
+            if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970  {
+                
+                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
+                        
+                    }
+                    
+                    
+                    
+                    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 = "↻"
+                    }
+                    
                 }
             }
-            else
-            {
+            
+        }
+        
+        var oText = "" as String
                
-                return
-            }
-            if self.consoleLogging == true {print("finish pump update")}
+               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()
         }
-        basalTask.resume()
-    }
     
     // Get last CAGE entry
     func loadCage(urlUser: String) {
@@ -400,13 +455,13 @@ class MainViewController: UIViewController, UITableViewDataSource {
         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 {
@@ -415,41 +470,38 @@ class MainViewController: UIViewController, UITableViewDataSource {
             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
-            {
-                
+            } else {
                 return
             }
         }
         task.resume()
     }
-    
+     
     // Parse Cage Data
     func updateCage(data: [cageData]) {
         if consoleLogging == true {print("in updateCage")}
         if data.count == 0 {
             return
         }
-        
-        var lastCageString = data[0].created_at
-        
+
+        let lastCageString = data[0].created_at
+
         let formatter = ISO8601DateFormatter()
         formatter.formatOptions = [.withFullDate,
-                                   .withTime,
-                                   .withDashSeparatorInDate,
-                                   .withColonSeparatorInTime]
-        if let cageTime = formatter.date(from: (lastCageString as! String))?.timeIntervalSince1970 {
+                                .withTime,
+                                .withDashSeparatorInDate,
+                                .withColonSeparatorInTime]
+        if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
             let now = NSDate().timeIntervalSince1970
             let secondsAgo = now - cageTime
-            let days = 24 * 60 * 60
+            //let days = 24 * 60 * 60
 
             let formatter = DateComponentsFormatter()
             formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
@@ -457,36 +509,34 @@ class MainViewController: UIViewController, UITableViewDataSource {
             formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
 
             let formattedDuration = formatter.string(from: secondsAgo)
-            tableData[8].value = formattedDuration ?? ""
+            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
-        
+        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"
+        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 {
@@ -495,37 +545,34 @@ class MainViewController: UIViewController, UITableViewDataSource {
             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
-            {
-                
+            } 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]
+                             .withTime,
+                             .withDashSeparatorInDate,
+                             .withColonSeparatorInTime]
         if let cageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
             let now = NSDate().timeIntervalSince1970
             let secondsAgo = now - cageTime
@@ -533,18 +580,70 @@ class MainViewController: UIViewController, UITableViewDataSource {
 
             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.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[7].value = formattedDuration ?? ""
+            tableData[6].value = formattedDuration ?? ""
         }
         infoTable.reloadData()
-
-    
     }
+     
+    // Need to figure out the date to pull only last 24 hours
+    // NOT IMPLEMENTED YET
+    func loadTempBasals(urlUser: String) {
+        var dayComponent    = DateComponents()
+        dayComponent.day    = -1 // For removing one day (yesterday): -1
+        let theCalendar     = Calendar.current
     
-    
+        let yesterday       = theCalendar.date(byAdding: dayComponent, to: Date())
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-mm-ddTHH:mm:ss"
+        //dateFormatter.timeZone = NSTimeZone(name: "UTC")
+        //let date: NSDate? = dateFormatter.dateFromString("2016-02-29 12:24:26")
+        //print(date)
+        
+        var urlStringBasal = urlUser + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=2020-06-02T22:46:26"
+        if token != "" {
+            urlStringBasal = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=2020-06-02T22:46:26"
+        }
+        
+        let escapedAddress = urlStringBasal.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
+        guard let urlBasal = URL(string: escapedAddress!) else {
+            return
+        }
+        
+        if consoleLogging == true {print("entered 2nd task.")}
+        var requestBasal = URLRequest(url: urlBasal)
+        requestBasal.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+        let basalTask = URLSession.shared.dataTask(with: requestBasal) { data, response, error in
+            if self.consoleLogging == true {print("in update 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")}
+        }
+        basalTask.resume()
+    }
+  
+    // NOT IMPLEMENTED YET
     func loadBoluses(urlUser: String){
         var calendar = Calendar.current
         let today = Date()
@@ -594,111 +693,7 @@ class MainViewController: UIViewController, UITableViewDataSource {
         }
         getTask.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
-                  //  BatteryText.text! += " " + String(format:"%.0f", upbat) + "%"
-                    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  {
-                
-                if let failure = lastLoopRecord["failureReason"] {
-                    tableData[6].value = "⚠"
-                }
-                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]
-                       // loopStatusText += " EBG " + bgOutputFormat(bg: prediction.last!, mmol: mmol)
-                    }
-                    
-                    if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
-                        if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
-                            if tempBasalTime > lastLoopTime {
-                                tableData[6].value = "⏀"
-                               } else {
-                                   tableData[6].value = "↻"
-                            }
-                        }
-                       
-                    }
-                    
-                }
-            }
-            
-        }
-        
-        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()
-        }
-        
+
     func stringFromTimeInterval(interval: TimeInterval) -> String {
         let interval = Int(interval)
         let minutes = (interval / 60) % 60

+ 37 - 0
LoopFollow/Controllers/SettingsViewController.swift

@@ -54,6 +54,43 @@ class SettingsViewController: FormViewController {
                         UserDefaultsRepository.appBadge.value = value
                 }
 
+        +++ Section("Alarm Settings")
+            <<< SwitchRow("overrideSystemOutputVolume"){ row in
+                row.title = "Override System Volume"
+                row.value = UserDefaultsRepository.overrideSystemOutputVolume.value
+            }.onChange { [weak self] row in
+                        guard let value = row.value else { return }
+                        UserDefaultsRepository.overrideSystemOutputVolume.value = value
+                }
+            <<< StepperRow("systemOutputVolume") { row in
+                  row.title = "Volume Level"
+                row.cell.stepper.stepValue = 0.05
+                  row.cell.stepper.minimumValue = 0
+                  row.cell.stepper.maximumValue = 1
+                  row.value = Double(UserDefaultsRepository.systemOutputVolume.value)
+                  row.hidden = "$overrideSystemOutputVolume == false"
+                    row.displayValueFor = { value in
+                    guard let value = value else { return nil }
+                    return "\(Int(value*100))%"
+                    }
+              }.onChange { [weak self] row in
+                      guard let value = row.value else { return }
+                      UserDefaultsRepository.systemOutputVolume.value = Float(value)
+              }
+            <<< StepperRow("fadeInTimeInterval") { row in
+                row.title = "Fade-in Seconds"
+                row.cell.stepper.stepValue = 5
+                row.cell.stepper.minimumValue = 0
+                row.cell.stepper.maximumValue = 60
+                row.value = Double(UserDefaultsRepository.fadeInTimeInterval.value)
+                row.displayValueFor = { value in
+                guard let value = value else { return nil }
+                return "\(Int(value))"
+                }
+            }.onChange { [weak self] row in
+                    guard let value = row.value else { return }
+                    UserDefaultsRepository.fadeInTimeInterval.value = TimeInterval(value)
+            }
             
         +++ Section("Graph Settings")
             <<< SwitchRow("switchRowDots"){ row in

+ 4 - 6
LoopFollow/Models/AlarmSound.swift

@@ -29,12 +29,10 @@ class AlarmSound {
     
     //static let volumeChangeDetector = VolumeChangeDetector()
     
-    static let fadeInTimeInterval = UserDefaultsValue<TimeInterval>(key: "fadeInTimeInterval", default: 0)  // fade-in time interval in seconds
-    
-    static let vibrate = UserDefaultsValue<Bool>(key: "vibrate", default: true)
-    
-    static let overrideSystemOutputVolume = UserDefaultsValue<Bool>(key: "overrideSystemOutputVolume", default: true)
-    static let systemOutputVolume = UserDefaultsValue<Float>(key: "systemOutputVolume", default: 0.7)
+    static let fadeInTimeInterval = UserDefaultsRepository.fadeInTimeInterval
+    static let vibrate = UserDefaultsRepository.vibrate
+    static let overrideSystemOutputVolume = UserDefaultsRepository.overrideSystemOutputVolume
+    static let systemOutputVolume = UserDefaultsRepository.systemOutputVolume
     
     fileprivate static var systemOutputVolumeBeforeOverride: Float?
     

+ 5 - 0
LoopFollow/repository/UserDefaults.swift

@@ -27,6 +27,11 @@ class UserDefaultsRepository {
     static let appBadge = UserDefaultsValue<Bool>(key: "appBadge", default: true)
     
     // Alarm Settings
+    static let systemOutputVolume = UserDefaultsValue<Float>(key: "systemOutputVolume", default: 0.5)
+    static let fadeInTimeInterval = UserDefaultsValue<TimeInterval>(key: "fadeInTimeInterval", default: 0)
+    static let vibrate = UserDefaultsValue<Bool>(key: "vibrate", default: true)
+    static let overrideSystemOutputVolume = UserDefaultsValue<Bool>(key: "overrideSystemOutputVolume", default: true)
+    
     
     // Alerts
     static let alertSnoozeAllTime = UserDefaultsValue<Date?>(key: "alertSnoozeAllTime", default: nil)