From e1aba0c430d2e18ef334a96586839e53cf0e8ab3 Mon Sep 17 00:00:00 2001 From: Chase Peeler Date: Thu, 19 Jan 2023 21:13:09 -0500 Subject: [PATCH] added support for specifying units and axis style as well as handling API returns in different formats Pulled API intereaction into its own class and wrote unit tests Moved settings into a separate struct which can be stored directly A little bit of cleanup. Got rid of AxisValues struct and just made it a method. Added test to confirm this fixes #8 --- GlucoseViewer.xcodeproj/project.pbxproj | 162 ++++++++++++++++- .../{BGReading.swift => BGLabel.swift} | 8 +- GlucoseViewer/BGLabelView.swift | 2 +- GlucoseViewer/GlucoseDetailsView.swift | 101 ++++++++--- GlucoseViewer/GlucoseViewerApp.swift | 116 +++++------- GlucoseViewer/GlucoseViewerSettings.swift | 112 ++++++++++++ GlucoseViewer/NightscoutAPI.swift | 157 ++++++++++++++++ GlucoseViewer/SettingsView.swift | 66 ++++--- GlucoseViewerTests/GlucoseViewerTests.swift | 38 ++++ GlucoseViewerTests/NightscoutAPITests.swift | 170 ++++++++++++++++++ GlucoseViewerTests/invalidjson.json | 1 + GlucoseViewerTests/settings.json | 1 + GlucoseViewerTests/stringdelta.json | 1 + GlucoseViewerTests/validjson.json | 37 ++++ 14 files changed, 839 insertions(+), 133 deletions(-) rename GlucoseViewer/{BGReading.swift => BGLabel.swift} (83%) create mode 100644 GlucoseViewer/GlucoseViewerSettings.swift create mode 100644 GlucoseViewer/NightscoutAPI.swift create mode 100644 GlucoseViewerTests/GlucoseViewerTests.swift create mode 100644 GlucoseViewerTests/NightscoutAPITests.swift create mode 100644 GlucoseViewerTests/invalidjson.json create mode 100644 GlucoseViewerTests/settings.json create mode 100644 GlucoseViewerTests/stringdelta.json create mode 100644 GlucoseViewerTests/validjson.json diff --git a/GlucoseViewer.xcodeproj/project.pbxproj b/GlucoseViewer.xcodeproj/project.pbxproj index f046c78..25636cd 100644 --- a/GlucoseViewer.xcodeproj/project.pbxproj +++ b/GlucoseViewer.xcodeproj/project.pbxproj @@ -10,27 +10,61 @@ AB1292DC2972064F00C7615F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1292DB2972064F00C7615F /* SettingsView.swift */; }; AB1292DF29732B9600C7615F /* GlucoseDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1292DE29732B9600C7615F /* GlucoseDetailsView.swift */; }; AB1292E12976155B00C7615F /* GlucoseViewerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1292E02976155B00C7615F /* GlucoseViewerApp.swift */; }; + AB1292E92977BB2A00C7615F /* GlucoseViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1292E82977BB2A00C7615F /* GlucoseViewerTests.swift */; }; + AB71105C2979F68000AD071E /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB71105B2979F68000AD071E /* NightscoutAPI.swift */; }; + AB71105E297A085000AD071E /* NightscoutAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB71105D297A085000AD071E /* NightscoutAPITests.swift */; }; + AB711060297A2B8200AD071E /* invalidjson.json in Resources */ = {isa = PBXBuildFile; fileRef = AB71105F297A2B8200AD071E /* invalidjson.json */; }; + AB711062297A2DA700AD071E /* validjson.json in Resources */ = {isa = PBXBuildFile; fileRef = AB711061297A2DA700AD071E /* validjson.json */; }; + AB711064297B7FF900AD071E /* stringdelta.json in Resources */ = {isa = PBXBuildFile; fileRef = AB711063297B7FF900AD071E /* stringdelta.json */; }; + AB711066297B8C9300AD071E /* GlucoseViewerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB711065297B8C9300AD071E /* GlucoseViewerSettings.swift */; }; + AB711068297B900500AD071E /* settings.json in Resources */ = {isa = PBXBuildFile; fileRef = AB711067297B900500AD071E /* settings.json */; }; ABED20CC296E5C8C00647363 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ABED20CB296E5C8C00647363 /* Assets.xcassets */; }; ABED20CF296E5C8C00647363 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ABED20CE296E5C8C00647363 /* Preview Assets.xcassets */; }; ABED20D9296F7C0F00647363 /* BGLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABED20D8296F7C0F00647363 /* BGLabelView.swift */; }; - ABED20DB296F7DF400647363 /* BGReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABED20DA296F7DF400647363 /* BGReading.swift */; }; + ABED20DB296F7DF400647363 /* BGLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABED20DA296F7DF400647363 /* BGLabel.swift */; }; ABED20DD296F7E2B00647363 /* BGDirectionEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABED20DC296F7E2B00647363 /* BGDirectionEnum.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + AB1292EA2977BB2A00C7615F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ABED20BC296E5C8B00647363 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ABED20C3296E5C8B00647363; + remoteInfo = GlucoseViewer; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ AB1292DB2972064F00C7615F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; AB1292DE29732B9600C7615F /* GlucoseDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDetailsView.swift; sourceTree = ""; }; AB1292E02976155B00C7615F /* GlucoseViewerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseViewerApp.swift; sourceTree = ""; }; + AB1292E62977BB2A00C7615F /* GlucoseViewerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GlucoseViewerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AB1292E82977BB2A00C7615F /* GlucoseViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseViewerTests.swift; sourceTree = ""; }; + AB71105B2979F68000AD071E /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = ""; }; + AB71105D297A085000AD071E /* NightscoutAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPITests.swift; sourceTree = ""; }; + AB71105F297A2B8200AD071E /* invalidjson.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = invalidjson.json; sourceTree = ""; }; + AB711061297A2DA700AD071E /* validjson.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = validjson.json; sourceTree = ""; }; + AB711063297B7FF900AD071E /* stringdelta.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = stringdelta.json; sourceTree = ""; }; + AB711065297B8C9300AD071E /* GlucoseViewerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseViewerSettings.swift; sourceTree = ""; }; + AB711067297B900500AD071E /* settings.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = settings.json; sourceTree = ""; }; ABED20C4296E5C8B00647363 /* GlucoseViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GlucoseViewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; ABED20CB296E5C8C00647363 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; ABED20CE296E5C8C00647363 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; ABED20D0296E5C8C00647363 /* GlucoseViewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlucoseViewer.entitlements; sourceTree = ""; }; ABED20D8296F7C0F00647363 /* BGLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGLabelView.swift; sourceTree = ""; }; - ABED20DA296F7DF400647363 /* BGReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGReading.swift; sourceTree = ""; }; + ABED20DA296F7DF400647363 /* BGLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGLabel.swift; sourceTree = ""; }; ABED20DC296F7E2B00647363 /* BGDirectionEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGDirectionEnum.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + AB1292E32977BB2A00C7615F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; ABED20C1296E5C8B00647363 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -41,10 +75,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + AB1292E72977BB2A00C7615F /* GlucoseViewerTests */ = { + isa = PBXGroup; + children = ( + AB1292E82977BB2A00C7615F /* GlucoseViewerTests.swift */, + AB71105D297A085000AD071E /* NightscoutAPITests.swift */, + AB71105F297A2B8200AD071E /* invalidjson.json */, + AB711061297A2DA700AD071E /* validjson.json */, + AB711063297B7FF900AD071E /* stringdelta.json */, + AB711067297B900500AD071E /* settings.json */, + ); + path = GlucoseViewerTests; + sourceTree = ""; + }; ABED20BB296E5C8B00647363 = { isa = PBXGroup; children = ( ABED20C6296E5C8B00647363 /* GlucoseViewer */, + AB1292E72977BB2A00C7615F /* GlucoseViewerTests */, ABED20C5296E5C8B00647363 /* Products */, ); sourceTree = ""; @@ -53,6 +101,7 @@ isa = PBXGroup; children = ( ABED20C4296E5C8B00647363 /* GlucoseViewer.app */, + AB1292E62977BB2A00C7615F /* GlucoseViewerTests.xctest */, ); name = Products; sourceTree = ""; @@ -64,11 +113,13 @@ ABED20D0296E5C8C00647363 /* GlucoseViewer.entitlements */, ABED20CD296E5C8C00647363 /* Preview Content */, ABED20D8296F7C0F00647363 /* BGLabelView.swift */, - ABED20DA296F7DF400647363 /* BGReading.swift */, + ABED20DA296F7DF400647363 /* BGLabel.swift */, ABED20DC296F7E2B00647363 /* BGDirectionEnum.swift */, AB1292DB2972064F00C7615F /* SettingsView.swift */, AB1292DE29732B9600C7615F /* GlucoseDetailsView.swift */, AB1292E02976155B00C7615F /* GlucoseViewerApp.swift */, + AB71105B2979F68000AD071E /* NightscoutAPI.swift */, + AB711065297B8C9300AD071E /* GlucoseViewerSettings.swift */, ); path = GlucoseViewer; sourceTree = ""; @@ -84,6 +135,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + AB1292E52977BB2A00C7615F /* GlucoseViewerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AB1292EC2977BB2A00C7615F /* Build configuration list for PBXNativeTarget "GlucoseViewerTests" */; + buildPhases = ( + AB1292E22977BB2A00C7615F /* Sources */, + AB1292E32977BB2A00C7615F /* Frameworks */, + AB1292E42977BB2A00C7615F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AB1292EB2977BB2A00C7615F /* PBXTargetDependency */, + ); + name = GlucoseViewerTests; + productName = GlucoseViewerTests; + productReference = AB1292E62977BB2A00C7615F /* GlucoseViewerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; ABED20C3296E5C8B00647363 /* GlucoseViewer */ = { isa = PBXNativeTarget; buildConfigurationList = ABED20D3296E5C8C00647363 /* Build configuration list for PBXNativeTarget "GlucoseViewer" */; @@ -112,6 +181,10 @@ LastUpgradeCheck = 1420; ORGANIZATIONNAME = "Peeler Coding, LLC"; TargetAttributes = { + AB1292E52977BB2A00C7615F = { + CreatedOnToolsVersion = 14.2; + TestTargetID = ABED20C3296E5C8B00647363; + }; ABED20C3296E5C8B00647363 = { CreatedOnToolsVersion = 14.2; }; @@ -131,11 +204,23 @@ projectRoot = ""; targets = ( ABED20C3296E5C8B00647363 /* GlucoseViewer */, + AB1292E52977BB2A00C7615F /* GlucoseViewerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + AB1292E42977BB2A00C7615F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB711062297A2DA700AD071E /* validjson.json in Resources */, + AB711060297A2B8200AD071E /* invalidjson.json in Resources */, + AB711068297B900500AD071E /* settings.json in Resources */, + AB711064297B7FF900AD071E /* stringdelta.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; ABED20C2296E5C8B00647363 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -148,14 +233,25 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + AB1292E22977BB2A00C7615F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB71105E297A085000AD071E /* NightscoutAPITests.swift in Sources */, + AB1292E92977BB2A00C7615F /* GlucoseViewerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; ABED20C0296E5C8B00647363 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( AB1292E12976155B00C7615F /* GlucoseViewerApp.swift in Sources */, - ABED20DB296F7DF400647363 /* BGReading.swift in Sources */, + ABED20DB296F7DF400647363 /* BGLabel.swift in Sources */, ABED20DD296F7E2B00647363 /* BGDirectionEnum.swift in Sources */, + AB711066297B8C9300AD071E /* GlucoseViewerSettings.swift in Sources */, AB1292DC2972064F00C7615F /* SettingsView.swift in Sources */, + AB71105C2979F68000AD071E /* NightscoutAPI.swift in Sources */, ABED20D9296F7C0F00647363 /* BGLabelView.swift in Sources */, AB1292DF29732B9600C7615F /* GlucoseDetailsView.swift in Sources */, ); @@ -163,7 +259,52 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + AB1292EB2977BB2A00C7615F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ABED20C3296E5C8B00647363 /* GlucoseViewer */; + targetProxy = AB1292EA2977BB2A00C7615F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + AB1292ED2977BB2A00C7615F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = EKBKRDG4Q9; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.1; + MARKETING_VERSION = 1.0; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.peelercoding.GlucoseViewerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GlucoseViewer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GlucoseViewer"; + }; + name = Debug; + }; + AB1292EE2977BB2A00C7615F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = EKBKRDG4Q9; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.peelercoding.GlucoseViewerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GlucoseViewer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GlucoseViewer"; + }; + name = Release; + }; ABED20D1296E5C8C00647363 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -288,7 +429,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "2201171944-alpha"; + CURRENT_PROJECT_VERSION = "2201172012-alpha"; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = EKBKRDG4Q9; ENABLE_HARDENED_RUNTIME = YES; @@ -323,7 +464,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "2201171944-alpha"; + CURRENT_PROJECT_VERSION = "2201172012-alpha"; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = EKBKRDG4Q9; ENABLE_HARDENED_RUNTIME = YES; @@ -351,6 +492,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + AB1292EC2977BB2A00C7615F /* Build configuration list for PBXNativeTarget "GlucoseViewerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB1292ED2977BB2A00C7615F /* Debug */, + AB1292EE2977BB2A00C7615F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; ABED20BF296E5C8B00647363 /* Build configuration list for PBXProject "GlucoseViewer" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/GlucoseViewer/BGReading.swift b/GlucoseViewer/BGLabel.swift similarity index 83% rename from GlucoseViewer/BGReading.swift rename to GlucoseViewer/BGLabel.swift index d0adbcd..140f8a6 100644 --- a/GlucoseViewer/BGReading.swift +++ b/GlucoseViewer/BGLabel.swift @@ -8,9 +8,9 @@ import Foundation struct BGLabel { - var glucose: Int = 0 + var glucose: StringableNumber = 0.0 var direction: BGDirection = BGDirection.Flat - var delta: Int = 0 + var delta: StringableNumber = 0.0 var status : Status = .Ok var hasError: Bool { get { @@ -20,13 +20,13 @@ struct BGLabel { var combined : String { get { - return String(glucose)+" "+directionArrow+" "+signedDelta + return glucose.string+" "+directionArrow+" "+signedDelta } } var signedDelta : String { get { - return (delta < 0 ? "" : "+")+String(delta) + return (delta.double < 0.0 ? "" : "+")+delta.string } } diff --git a/GlucoseViewer/BGLabelView.swift b/GlucoseViewer/BGLabelView.swift index c0cd821..f605797 100644 --- a/GlucoseViewer/BGLabelView.swift +++ b/GlucoseViewer/BGLabelView.swift @@ -21,7 +21,7 @@ struct BGLabelView: View { } struct BGLabelView_Previews: PreviewProvider { - @State static var bg = BGLabel(glucose: 100, direction: .Flat, delta: 2) + @State static var bg = BGLabel(glucose: "101", direction: .Flat, delta: 2) static var previews: some View { BGLabelView(bglabel: $bg) } diff --git a/GlucoseViewer/GlucoseDetailsView.swift b/GlucoseViewer/GlucoseDetailsView.swift index 8a67de0..e93228a 100644 --- a/GlucoseViewer/GlucoseDetailsView.swift +++ b/GlucoseViewer/GlucoseDetailsView.swift @@ -11,8 +11,7 @@ import Charts struct GlucoseDetailsView: View { @Environment(\.openURL) private var opener - @Binding var baseUrl: String - @Binding var token: String + @Binding var settings:GlucoseViewerSettings @ObservedObject var bgData: BGData; @State private var action: Int? = 1 @State private var showModal = false @@ -21,42 +20,45 @@ struct GlucoseDetailsView: View { VStack { if(bgData.hasData){ - let yAxisValues: [Int] = stride(from: bgData.minBG-10, to:bgData.maxBG+20,by: 10).map {$0} - + let yAxisValues = getYAxisValues() GroupBox ( "Glucose") { Chart (bgData.bgs.reversed()) { LineMark( x: .value("Time", $0.formattedDate), - y: .value("Glucose", $0.glucose) + y: .value("Glucose", $0.glucose.double) ).symbol(.circle).alignsMarkStylesWithPlotArea() }.chartYAxis{ AxisMarks(values: yAxisValues) - }.chartYScale(domain: ClosedRange(uncheckedBounds: (lower: yAxisValues.min()!, upper: yAxisValues.max()!))) - .frame(minWidth: 700) - - - - } - } + }.chartYScale(domain: + ClosedRange(uncheckedBounds: ( + lower: yAxisValues.min()!, + upper: yAxisValues.max()! + ) //end uncheckedBounds + ) //end ClosedRange + ).frame(minWidth: 700) //end chart + } //end groupbox + } // end if HStack(alignment: .bottom) { Button("Settings"){ showModal = true } Button("Open Nightscout"){ - self.openURL(self.baseUrl) + self.openURL(self.settings.url) }.scaledToFill() + Button("Quit"){ NSApplication.shared.terminate(nil) }.scaledToFill() - }.padding() + + }.padding() // end hstack }.frame(maxWidth: .infinity,maxHeight: .infinity).opacity(100) .sheet(isPresented: $showModal,onDismiss:{ - print(baseUrl) + print(settings.url) } - ){ - SettingsView(url: $baseUrl, token: $token) + ) { + SettingsView(settings: $settings) }.opacity(0.99) } @@ -72,6 +74,31 @@ struct GlucoseDetailsView: View { opener(url) } + func getYAxisValues() -> [Double]{ + var values: [Double] = [] + switch settings.axisStyle { + case .fixed: + switch settings.units { + case .mgdL: + values = stride(from: 50, to:300,by: 25).map {Double($0)} + case .mmolL: + values = stride(from: 2.8, to:16.6,by: 0.60).map {$0} + } + case .dynamic: + switch settings.units { + case .mgdL: + values = stride(from: bgData.minBG.int-10, to:bgData.maxBG.int+20,by: 10).map {Double($0)} + case .mmolL: + values = stride(from: bgData.minBG.double-1.0, to:bgData.maxBG.double+1.5,by: 0.5).map {$0} + } + } + + values.sort() + + return values + + } + } struct GlucoseDetailsView_Previews: PreviewProvider { @@ -96,10 +123,30 @@ struct GlucoseDetailsView_Previews: PreviewProvider { .add(BGDatum(glucose:150,datetime:1673736077000)) .add(BGDatum(glucose:145,datetime:1673735777000)) .add(BGDatum(glucose:138,datetime:1673735476000)) - @State static var token = "" - @State static var baseUrl = "" + // BGData().add(BGDatum(glucose:4.0,datetime:1673741177000)) + // .add(BGDatum(glucose:4.5,datetime:1673740877000)) + // .add(BGDatum(glucose:4.6,datetime:1673740577000)) + // .add(BGDatum(glucose:4.7,datetime:1673740277000)) + // .add(BGDatum(glucose:4.1,datetime:1673739977000)) + // .add(BGDatum(glucose:4.0,datetime:1673739677000)) + // .add(BGDatum(glucose:4.0,datetime:1673739377000)) + // .add(BGDatum(glucose:4.2,datetime:1673739076000)) + // .add(BGDatum(glucose:4.5,datetime:1673738777000)) + // .add(BGDatum(glucose:4.0,datetime:1673738477000)) + // .add(BGDatum(glucose:4.5,datetime:1673738177000)) + // .add(BGDatum(glucose:4.5,datetime:1673737876000)) + // .add(BGDatum(glucose:4.6,datetime:1673737577000)) + // .add(BGDatum(glucose:5.1,datetime:1673737276000)) + // .add(BGDatum(glucose:4.5,datetime:1673736977000)) + // .add(BGDatum(glucose:5.6,datetime:1673736676000)) + // .add(BGDatum(glucose:4.7,datetime:1673736377000)) + // .add(BGDatum(glucose:6.9,datetime:1673736077000)) + // .add(BGDatum(glucose:4.5,datetime:1673735777000)) + // .add(BGDatum(glucose:4.5,datetime:1673735476000)) + // + @State static var settings = GlucoseViewerSettings(units: .mgdL,axisStyle: .dynamic) static var previews: some View { - GlucoseDetailsView(baseUrl: $baseUrl, token: $token, bgData: bgs) + GlucoseDetailsView(settings:$settings, bgData: bgs) } } @@ -110,17 +157,17 @@ struct BGDatum: Identifiable { return datetime } } - var glucose: Int + var glucose: StringableNumber var datetime: Date - init(glucose:Int,datetime:Date){ + init(glucose:StringableNumber,datetime:Date){ self.glucose = glucose self.datetime = datetime } - init(glucose: Int, datetime:Double){ + init(glucose: StringableNumber, datetime:Double){ var d = datetime - + if(d > 9999999999.0){ d = d/1000.0 } @@ -143,13 +190,13 @@ class BGData :ObservableObject { @Published var bgs: [BGDatum] var hasData:Bool = false - var minBG: Int { + var minBG: StringableNumber { get { return bgs.min {$0.glucose < $1.glucose}!.glucose } } - var maxBG: Int { + var maxBG: StringableNumber { get { return bgs.max {$0.glucose < $1.glucose}!.glucose } @@ -168,7 +215,7 @@ class BGData :ObservableObject { func replace(with bgs:[APIBgs]){ self.bgs = [] bgs.forEach(){ bg in - let g = Int(bg.sgv)! + let g = bg.sgv var d = bg.datetime if(d > 9999999999.0){ d = d/1000.0 diff --git a/GlucoseViewer/GlucoseViewerApp.swift b/GlucoseViewer/GlucoseViewerApp.swift index 289e29b..c1dbf80 100644 --- a/GlucoseViewer/GlucoseViewerApp.swift +++ b/GlucoseViewer/GlucoseViewerApp.swift @@ -13,94 +13,76 @@ import SwiftUI @main struct GlucoseViewerApp: App { @State var bg:BGLabel = BGLabel(status: .Ok) - @AppStorage("url") private var baseUrl : String = "" - @AppStorage("token") private var token : String = "" - @State var showingPopup = false + @AppStorage("settings") private var settings = GlucoseViewerSettings() @StateObject var bgs: BGData = BGData() + private var api = NightscoutAPI() - @State var a = false; var body: some Scene { MenuBarExtra(content:{ - GlucoseDetailsView(baseUrl: $baseUrl, token: $token, bgData: bgs).frame(maxWidth: .infinity) + GlucoseDetailsView(settings:$settings, bgData: bgs).frame(maxWidth: .infinity) }, label: { - BGLabelView(bglabel: $bg).onAppear(perform: loadData) + + BGLabelView(bglabel: $bg).task{ + //url and token were originally stored as their own values + //but were moved to the settings struct which is stored directly + //the following checks if the old storage still exists, and if it does, it will + //copy that value to the new storage and then delete the old key + if let url = UserDefaults.standard.string(forKey: "url") { + settings.url = url + UserDefaults.standard.removeObject(forKey: "url") + } + + if let token = UserDefaults.standard.string(forKey: "token"){ + settings.token = token + UserDefaults.standard.removeObject(forKey: "token") + } + + await self.loadData() + + } + }).menuBarExtraStyle(.window).windowStyle(.hiddenTitleBar) } - func loadData(){ - if(self.baseUrl.isEmpty){ - self.bg.status = .NoUrl - Timer.scheduledTimer(withTimeInterval: 15, repeats: false){timer in - loadData() - } - return - } - var urlString = self.baseUrl + /// triggers API call and loads data + func loadData() async { - if(urlString.last != "/"){ - urlString += "/" - } - - urlString += "pebble?count=20" - if(!self.token.isEmpty){ - urlString += "&token="+self.token - } - guard let url = URL(string: urlString) - else { - self.bg.status = .Error - return + var interval = 15.0 + do { + let r = try await api.loadData(self.settings.url,token: self.settings.token) + self.bg.direction = BGDirection(rawValue: r.bgs[0].direction)! + self.bg.glucose = r.bgs[0].sgv + self.bg.delta = r.bgs[0].bgdelta! + self.bgs.replace(with: r.bgs) + self.bg.status = .Ok + interval = 5.0*60.0 + } catch APIError.EmptyUrl { + self.bg.status = .NoUrl + } catch APIError.InvalidUrl(let url){ + print("Invalid URL: \(url)") + } catch APIError.FailedRequest { + print("The request failed") + } catch APIError.DecodeError(let e){ + print("Decoding Error \(e)") + } catch { + print("Unknown error: \(error)") } - - var interval = 5.0*60.0 - URLSession.shared.dataTask(with: url) { (data, _, _) in - DispatchQueue.main.async { - do { - if(data != nil){ - let r: APIData = try JSONDecoder().decode(APIData.self, from: data!) - - self.bg.direction = BGDirection(rawValue: r.bgs[0].direction)! - self.bg.glucose = Int(r.bgs[0].sgv)! - self.bg.delta = r.bgs[0].bgdelta! - self.bgs.replace(with: r.bgs) - self.bg.status = .Ok - } else { - self.bg.status = .Error - interval = 15.0 - } - } catch { - self.bg.status = .Error - print(error) - interval = 15.0 - } - Timer.scheduledTimer(withTimeInterval: interval, repeats: false){timer in - loadData() - } + Timer.scheduledTimer(withTimeInterval: interval, repeats: false){timer in + Task { + await loadData() } - }.resume() + } } + } -struct APIStatus : Codable { - var now: Int -} -struct APIBgs : Codable { - var sgv: String - var trend: Int - var direction: String - var datetime: Double - var bgdelta: Int? -} -struct APIData : Codable { - var status: [APIStatus] - var bgs: [APIBgs] -} diff --git a/GlucoseViewer/GlucoseViewerSettings.swift b/GlucoseViewer/GlucoseViewerSettings.swift new file mode 100644 index 0000000..ab8307a --- /dev/null +++ b/GlucoseViewer/GlucoseViewerSettings.swift @@ -0,0 +1,112 @@ +// +// GlucoseViewerSettings.swift +// GlucoseViewer +// +// Created by Chase Peeler on 1/20/23. +// Copyright © 2023 Peeler Coding, LLC. All rights reserved. +// + +import Foundation + +/// Holds the settings for the app. Can be stored directly in application storage. +struct GlucoseViewerSettings { + + + + /// Glucose units. mg/dL or mmol/L + enum Units:String { + case mgdL + case mmolL + } + + /// Whether the graph is displayed using fixed values for the y-axis or + /// values that adjust based on the highest and lowest values retrieved + enum AxisStyle:String { + case fixed + case dynamic + } + + /// The base url of the nightscout instance + var url:String = "" + + /// An authentication token. Leave blank if none + var token:String = "" + + /// The units that glucose values are meaured in + var units: Units = .mgdL + + /// The style the graph is displayed in + var axisStyle:AxisStyle = .dynamic + + /// Empty url and token, .mgdL units, and .dynamic axisStyle + init(){ + self.url = "" + self.token = "" + self.units = .mgdL + self.axisStyle = .dynamic + } + + init(url: String = "", token: String = "", units: GlucoseViewerSettings.Units = .mgdL, axisStyle: GlucoseViewerSettings.AxisStyle = .dynamic) { + self.url = url + self.token = token + self.units = units + self.axisStyle = axisStyle + } + + + +} + +extension GlucoseViewerSettings : Codable { + + /* Codable */ + + enum CodingKeys:String,CodingKey { + case url + case token + case units + case axis + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(url, forKey: .url) + try container.encode(token, forKey: .token) + try container.encode(units.rawValue, forKey: .units) + try container.encode(axisStyle.rawValue,forKey: .axis) + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.url = try String(values.decode(String.self, forKey: .url)) + self.token = try String(values.decode(String.self, forKey: .token)) + self.units = try Units(rawValue: values.decode(String.self, forKey: .units)) ?? .mgdL + self.axisStyle = try AxisStyle(rawValue: values.decode(String.self, forKey:.axis)) ?? .dynamic + + } + +} + +extension GlucoseViewerSettings:RawRepresentable { + /* RawRepresentable */ + + init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode(GlucoseViewerSettings.self, from: data) + else { + return nil + } + self = result + } + + var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } + + +} diff --git a/GlucoseViewer/NightscoutAPI.swift b/GlucoseViewer/NightscoutAPI.swift new file mode 100644 index 0000000..9c1a10f --- /dev/null +++ b/GlucoseViewer/NightscoutAPI.swift @@ -0,0 +1,157 @@ +// +// NightscoutAPI.swift +// GlucoseViewer +// +// Created by Chase Peeler on 1/19/23. +// Copyright © 2023 Peeler Coding, LLC. All rights reserved. +// + +import Foundation +import SwiftUI + + + +struct APIStatus : Codable { + var now: Double +} + +struct StringableNumber: Codable,ExpressibleByFloatLiteral,ExpressibleByIntegerLiteral,ExpressibleByStringLiteral,Comparable { + + var string: String + var int: Int + var double: Double + + /* Comparable */ + + static func < (lhs: StringableNumber, rhs: StringableNumber) -> Bool { + return lhs.double < rhs.double + } + + /* ExxpressByFloatLiteral */ + + init(floatLiteral value: Double){ + self.string = String(value) + self.int = Int(value) + self.double = value + } + + /* ExpressByIntegerLiteral */ + + init(integerLiteral value: Int){ + self.string = String(value) + self.int = value + self.double = Double(value) + } + + /* ExpressByStringLiteral */ + + init(stringLiteral value: String){ + self.string = value + self.int = Int(value) ?? 0 + self.double = Double(value) ?? 0.0 + } + + /* Codable*/ + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.double = 0.0 + self.int = 0 + self.string = "0" + + if let string = try? container.decode(String.self) { + if let d = Double(string) { + self.double = d + self.int = Int(d) + self.string = string + } + } else if let int = try? container.decode(Int.self){ + self.int = int + self.double = Double(int) + self.string = String(int) + } else if let double = try? container.decode(Double.self){ + self.double = double + self.int = Int(double) + self.string = String(double) + } + } + + // We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.double) + } +} + +struct APIBgs : Codable { + var sgv: StringableNumber + var trend: Int + var direction: String + var datetime: Double + var bgdelta: StringableNumber? +} + + +struct APIData : Codable { + var status: [APIStatus] + var bgs: [APIBgs] +} + +enum APIError: Error { + case EmptyUrl + case InvalidUrl(url:String) + case FailedRequest + case DecodeError(error:Error) +} + + +struct NightscoutAPI{ + private let session: NightscoutUrlSessionProtocol + + init(session: NightscoutUrlSessionProtocol = URLSession.shared) { + self.session = session + } + + func loadData(_ baseUrl:String,token: String = "") async throws -> APIData { + + if(baseUrl.isEmpty){ + throw APIError.EmptyUrl + } + + var urlString = baseUrl + + if(urlString.last != "/"){ + urlString += "/" + } + + urlString += "pebble?count=20" + if(!token.isEmpty){ + urlString += "&token="+token + } + + guard let url = URL(string: urlString) else { + throw APIError.InvalidUrl(url: urlString) + } + + let (data,response) = try await session.data(from: url,delegate: nil) + guard (response as? HTTPURLResponse)?.statusCode == 200 else { + throw APIError.FailedRequest + } + + do { + let r: APIData = try JSONDecoder().decode(APIData.self, from: data) + return r + } catch { + throw APIError.DecodeError(error: error) + } + + + } + +} + +protocol NightscoutUrlSessionProtocol { + func data(from url: URL, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse) +} + +extension URLSession : NightscoutUrlSessionProtocol {} diff --git a/GlucoseViewer/SettingsView.swift b/GlucoseViewer/SettingsView.swift index e98cbe1..f6ab428 100644 --- a/GlucoseViewer/SettingsView.swift +++ b/GlucoseViewer/SettingsView.swift @@ -12,53 +12,63 @@ import Combine struct SettingsView: View { @Environment(\.presentationMode) var presentation - @Binding var url : String - @Binding var token: String - - @State var originalUrl: String - @State var originalToken: String + @Binding var settings :GlucoseViewerSettings + @State var originalSettings:GlucoseViewerSettings let userDefaults = UserDefaults.standard - init(url: Binding, token: Binding){ - self._url = url - self._token = token - - self._originalUrl = State(initialValue: url.wrappedValue) - self._originalToken = State(initialValue: token.wrappedValue) + init(settings:Binding){ + self._settings = settings + self._originalSettings = State(initialValue: settings.wrappedValue) } var body: some View { VStack { Text("Glucose Toolbar Viewer Settings").font(.title) Form { - TextField("Nightscout URL",text: $originalUrl) - TextField("API Token",text: $originalToken) - HStack { - Button("Save") { - self.url = self.originalUrl - self.token = self.originalToken - userDefaults.set(self.url, forKey: "url") - userDefaults.set(self.token, forKey:"token") + VStack { + TextField(text: $originalSettings.url) { + Text("Nightscount URL").bold() - self.presentation.wrappedValue.dismiss() } - Button("Cancel"){ - // self.url = self.originalUrl - // self.token = self.originalToken - self.presentation.wrappedValue.dismiss() + TextField(text: $originalSettings.token) { + Text("API Token").bold().padding(EdgeInsets(top: 0, leading: 44, bottom: 0, trailing: 0)) } - }.frame(maxWidth: .infinity, alignment: .trailing) + + + HStack { + Picker(selection: $originalSettings.units, label: Text("Units:").bold()) { + Text("mg/dL").tag(GlucoseViewerSettings.Units.mgdL) + Text("mmol/L").tag(GlucoseViewerSettings.Units.mmolL) + }.pickerStyle(.radioGroup).horizontalRadioGroupLayout() + Spacer() + + Picker(selection: $originalSettings.axisStyle, label: Text("Axis Style:").bold() ) { + Text("Dynamic").tag(GlucoseViewerSettings.AxisStyle.dynamic) + Text("Fixed").tag(GlucoseViewerSettings.AxisStyle.fixed) + }.pickerStyle(.radioGroup).horizontalRadioGroupLayout().frame(alignment: .trailing) + }.frame(maxWidth: .infinity,alignment:.center) + HStack { + Button("Save") { + //since self.settings is a bound appstorage property + //any updates to it will get saved automatically + self.settings = self.originalSettings + self.presentation.wrappedValue.dismiss() + } + Button("Cancel"){ + self.presentation.wrappedValue.dismiss() + } + }.frame(maxWidth: .infinity, alignment: .trailing) + }.frame(maxWidth: .infinity).textFieldStyle(.roundedBorder) }.frame(width: 500).padding(5) }.padding(10) } } struct EnterUrlView_Previews: PreviewProvider { - @State static var b = "" - @State static var u = "d" + @State static var settings = GlucoseViewerSettings() static var previews: some View { - SettingsView(url: $u, token: $b) + SettingsView(settings: $settings) } } diff --git a/GlucoseViewerTests/GlucoseViewerTests.swift b/GlucoseViewerTests/GlucoseViewerTests.swift new file mode 100644 index 0000000..871e123 --- /dev/null +++ b/GlucoseViewerTests/GlucoseViewerTests.swift @@ -0,0 +1,38 @@ +// +// GlucoseViewerTests.swift +// GlucoseViewerTests +// +// Created by Chase Peeler on 1/18/23. +// Copyright © 2023 Peeler Coding, LLC. All rights reserved. +// + +import XCTest +@testable import GlucoseViewer + +final class GlucoseViewerTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + + let bgl = BGLabel(glucose: 100, direction: BGDirection.Flat, delta: -2, status: BGLabel.Status.Ok) + let arrow = bgl.directionArrow + XCTAssertEqual(arrow, "→") + + + } + + + +} diff --git a/GlucoseViewerTests/NightscoutAPITests.swift b/GlucoseViewerTests/NightscoutAPITests.swift new file mode 100644 index 0000000..dce5c6a --- /dev/null +++ b/GlucoseViewerTests/NightscoutAPITests.swift @@ -0,0 +1,170 @@ +// +// NightscoutAPITests.swift +// GlucoseViewerTests +// +// Created by Chase Peeler on 1/19/23. +// Copyright © 2023 Peeler Coding, LLC. All rights reserved. +// + +import XCTest +@testable import GlucoseViewer + +final class NightscoutAPITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testEmptyUrl() async throws { + + let data = Data() + let response = URLResponse() + + let session = MockURLSession(mockData: data, mockResponse: response) + let api = NightscoutAPI(session: session) + let e = expectation(description: "Empty URL") + do { + let _ = try await api.loadData("") + } catch APIError.EmptyUrl { + e.fulfill() + } + + wait(for: [e], timeout: 3) + } + + func testInvalidUrl() async throws { + + let data = Data() + let response = URLResponse() + + let session = MockURLSession(mockData: data, mockResponse: response) + let api = NightscoutAPI(session: session) + let e = expectation(description: "Invalid URL") + do { + let _ = try await api.loadData("htp:\\") + } catch APIError.InvalidUrl(let url) { + XCTAssertEqual(url,"htp:\\/pebble?count=20") + e.fulfill() + } + + wait(for: [e], timeout: 3) + } + + func testFailedRequest() async throws { + + let data = Data() + + let response = HTTPURLResponse(url: URL(string: "http://www.example.com")!, statusCode: 404, httpVersion: nil, headerFields: nil) + + let session = MockURLSession(mockData: data, mockResponse: response!) + let api = NightscoutAPI(session: session) + let e = expectation(description: "Failed Request") + do { + let _ = try await api.loadData("http://example.com") + } catch APIError.FailedRequest { + e.fulfill() + } + + wait(for: [e], timeout: 3) + } + + func testFailedDecode() async throws { + + let data = loadJsonData(file: "invalidjson")! + + let response = HTTPURLResponse(url: URL(string: "http://www.example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + let session = MockURLSession(mockData: data, mockResponse: response!) + let api = NightscoutAPI(session: session) + let e = expectation(description: "Failed DEcoding") + do { + let _ = try await api.loadData("http://example.com") + } catch APIError.DecodeError( _) { + e.fulfill() + } + + wait(for: [e], timeout: 3) + } + + func testSuccess() async throws { + let data = loadJsonData(file: "validjson")! + + let response = HTTPURLResponse(url: URL(string: "http://www.example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + let session = MockURLSession(mockData: data, mockResponse: response!) + let api = NightscoutAPI(session: session) + let e = expectation(description: "Valid") + do { + let r = try await api.loadData("http://example.com") + XCTAssertEqual(r.status.count,1) + XCTAssertEqual(r.status[0].now, 1674180158028) + XCTAssertEqual(r.bgs.count, 3) + XCTAssertEqual(r.bgs[0].sgv, "121") + XCTAssertEqual(r.bgs[0].trend, 4) + XCTAssertEqual(r.bgs[0].direction, "Flat") + + e.fulfill() + } catch { + + } + + wait(for: [e], timeout: 3) + } + + /// Test for issue [#8](https://github.com/chasepeeler/GlucoseViewer/issues/8) + func testStringDeltaValue() async throws { + let data = loadJsonData(file: "stringdelta")! + + let response = HTTPURLResponse(url: URL(string: "http://www.example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + let session = MockURLSession(mockData: data, mockResponse: response!) + let api = NightscoutAPI(session: session) + let e = expectation(description: "Valid") + do { + let r = try await api.loadData("http://example.com") + XCTAssertEqual(r.bgs[0].bgdelta?.string, "-0.8") + e.fulfill() + } catch { + + } + + wait(for: [e], timeout: 3) + } + + private func loadJsonData(file: String) -> Data? { + //1 + if let jsonFilePath = Bundle(for: type(of: self)).path(forResource: file, ofType: "json") { + let jsonFileURL = URL(fileURLWithPath: jsonFilePath) + //2 + if let jsonData = try? Data(contentsOf: jsonFileURL) { + return jsonData + } + } + //3 + return nil + } + +} + +class MockURLSession : NightscoutUrlSessionProtocol { + + var mockData:Data + var mockResponse:URLResponse + + init(mockData: Data, mockResponse: URLResponse) { + self.mockData = mockData + self.mockResponse = mockResponse + } + + func data(from url: URL, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse) { + return (self.mockData,self.mockResponse) + } + + +} + + diff --git a/GlucoseViewerTests/invalidjson.json b/GlucoseViewerTests/invalidjson.json new file mode 100644 index 0000000..212992d --- /dev/null +++ b/GlucoseViewerTests/invalidjson.json @@ -0,0 +1 @@ +["] diff --git a/GlucoseViewerTests/settings.json b/GlucoseViewerTests/settings.json new file mode 100644 index 0000000..616a667 --- /dev/null +++ b/GlucoseViewerTests/settings.json @@ -0,0 +1 @@ +{"axis":"dynamic","units":"mgdL","url":"https:\/\/nightscout.chasepeeler.com","token":""} diff --git a/GlucoseViewerTests/stringdelta.json b/GlucoseViewerTests/stringdelta.json new file mode 100644 index 0000000..ee65d16 --- /dev/null +++ b/GlucoseViewerTests/stringdelta.json @@ -0,0 +1 @@ +{"status":[{"now":1674219321863}],"bgs":[{"sgv":"6.9","trend":5,"direction":"FortyFiveDown","datetime":1674219262021,"bgdelta":"-0.8","battery":"85","iob":"1.25","bwp":"-1.49","bwpo":-2,"cob":23.2}],"cals":[]} diff --git a/GlucoseViewerTests/validjson.json b/GlucoseViewerTests/validjson.json new file mode 100644 index 0000000..b1fc859 --- /dev/null +++ b/GlucoseViewerTests/validjson.json @@ -0,0 +1,37 @@ +{ + "status": + [ + { + "now":1674180158028 + + } + ], + "bgs": + [ + { + "sgv":"121", + "trend":4, + "direction":"Flat", + "datetime":1674180095000, + "bgdelta":4 + + }, + { + "sgv":"117", + "trend":4, + "direction":"Flat", + "datetime":1674179795000 + + }, + { + "sgv":"108", + "trend":4, + "direction":"Flat", + "datetime":1674179495000 + + } + ], + "cals": + [ + ] +}