readings on chart

This commit is contained in:
Johan Degraeve 2019-10-29 19:08:21 +01:00
parent 1317c54c26
commit cb1bdd7a12
7 changed files with 185 additions and 103 deletions

View File

@ -22,6 +22,7 @@
F80859272364355F00F3829D /* ConstantsGlucoseChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80859262364355F00F3829D /* ConstantsGlucoseChart.swift */; };
F80859292364D61B00F3829D /* UserDefaults+charts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80859282364D61B00F3829D /* UserDefaults+charts.swift */; };
F808592B23660F4500F3829D /* BidirectionalCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F808592A23660F4500F3829D /* BidirectionalCollection.swift */; };
F808592D23677D6A00F3829D /* ChartPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = F808592C23677D6A00F3829D /* ChartPoint.swift */; };
F81D6D4822BD5F62005EFAE2 /* DexcomShareUploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81D6D4722BD5F62005EFAE2 /* DexcomShareUploadManager.swift */; };
F81D6D4E22BFC762005EFAE2 /* TextsDexcomShareTestResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81D6D4D22BFC762005EFAE2 /* TextsDexcomShareTestResult.swift */; };
F81D6D5222C27F18005EFAE2 /* BgReading+DexcomShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81D6D5122C27F18005EFAE2 /* BgReading+DexcomShare.swift */; };
@ -260,6 +261,7 @@
F80859262364355F00F3829D /* ConstantsGlucoseChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsGlucoseChart.swift; sourceTree = "<group>"; };
F80859282364D61B00F3829D /* UserDefaults+charts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+charts.swift"; sourceTree = "<group>"; };
F808592A23660F4500F3829D /* BidirectionalCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BidirectionalCollection.swift; sourceTree = "<group>"; };
F808592C23677D6A00F3829D /* ChartPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartPoint.swift; sourceTree = "<group>"; };
F81D6D4522B67F55005EFAE2 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/SpeakReading.strings; sourceTree = "<group>"; };
F81D6D4722BD5F62005EFAE2 /* DexcomShareUploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomShareUploadManager.swift; sourceTree = "<group>"; };
F81D6D4D22BFC762005EFAE2 /* TextsDexcomShareTestResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsDexcomShareTestResult.swift; sourceTree = "<group>"; };
@ -678,6 +680,7 @@
F8B3A857227F6971004BA588 /* UISwitch.swift */,
F8BDD4232218790E006EAB84 /* UserDefaults.swift */,
F80859282364D61B00F3829D /* UserDefaults+charts.swift */,
F808592C23677D6A00F3829D /* ChartPoint.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1758,6 +1761,7 @@
F8A54AE222D911BA00934E7A /* ResetMessage.swift in Sources */,
F821CF5F229BF43A005C1E43 /* ApplicationManager.swift in Sources */,
F8B3A834227F08AC004BA588 /* PickerViewData.swift in Sources */,
F808592D23677D6A00F3829D /* ChartPoint.swift in Sources */,
F8B3A79522635A25004BA588 /* AlertType+CoreDataProperties.swift in Sources */,
F8A389BC231713580010F405 /* M5StackTransmitterOpCode.swift in Sources */,
F8B3A84C227F090E004BA588 /* SettingsViewController.swift in Sources */,

View File

@ -67,6 +67,6 @@ enum ConstantsGlucoseChart {
static let axisTitleLabelsToLabelsSpacing: CGFloat = 0
/// diameter of the circle for blood glucose readings
static let glucoseCircleDiameter: CGFloat = 7
static let glucoseCircleDiameter: CGFloat = 5
}

View File

@ -12,13 +12,21 @@ class BgReadingsAccessor {
/// CoreDataManager to use
private let coreDataManager:CoreDataManager
/// to be used when fetch request needs to run on a background thread
private let privateManagedObjectContext: NSManagedObjectContext
// MARK: - initializer
init(coreDataManager:CoreDataManager) {
self.coreDataManager = coreDataManager
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateManagedObjectContext.persistentStoreCoordinator = coreDataManager.mainManagedObjectContext.persistentStoreCoordinator
}
// MARK: - functions
// MARK: - public functions
/// Gives readings for which calculatedValue != 0, rawdata != 0, matching sensorid if sensorid not nil, with maximumDays old
///
@ -97,6 +105,43 @@ class BgReadingsAccessor {
}
}
/// gets readings on a managedObjectContact that is created with concurrencyType: .privateQueueConcurrencyType
/// - parameters:
/// - to : if specified, only return readings with timestamp smaller than fromDate
/// - from : if specified, only return readings with timestamp greater than fromDate
func getBgReadingOnPrivateManagedObjectContext(from: Date?, to: Date?) -> [BgReading] {
let fetchRequest: NSFetchRequest<BgReading> = BgReading.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(BgReading.timeStamp), ascending: false)]
// create predicate
if let from = from, to == nil {
let predicate = NSPredicate(format: "timeStamp > %@", NSDate(timeIntervalSince1970: from.timeIntervalSince1970))
fetchRequest.predicate = predicate
} else if let to = to, from == nil {
let predicate = NSPredicate(format: "timeStamp < %@", NSDate(timeIntervalSince1970: to.timeIntervalSince1970))
fetchRequest.predicate = predicate
} else if let to = to, let from = from {
let predicate = NSPredicate(format: "timeStamp < %@ AND timeStamp > %@", NSDate(timeIntervalSince1970: to.timeIntervalSince1970), NSDate(timeIntervalSince1970: from.timeIntervalSince1970))
fetchRequest.predicate = predicate
}
var bgReadings = [BgReading]()
privateManagedObjectContext.performAndWait {
do {
// Execute Fetch Request
bgReadings = try fetchRequest.execute()
} catch {
let fetchError = error as NSError
trace("in getBgReadingOnPrivateManagedObjectContext, Unable to Execute BgReading Fetch Request : %{public}@", log: self.log, type: .error, fetchError.localizedDescription)
}
}
return bgReadings
}
// MARK: - private helper functions
/// returnvalue can be empty array
@ -132,4 +177,5 @@ class BgReadingsAccessor {
return bgReadings
}
}

View File

@ -0,0 +1,18 @@
import Foundation
import SwiftCharts
extension ChartPoint {
convenience init?(bgReading: BgReading, formatter: DateFormatter, unitIsMgDl: Bool) {
if bgReading.calculatedValue > 0 {
self.init(
x: ChartAxisValueDate(date: bgReading.timeStamp, formatter: formatter),
y: ChartAxisValueDouble(bgReading.calculatedValue.mgdlToMmol(mgdl: unitIsMgDl))
)
} else {
return nil
}
}
}

View File

@ -12,41 +12,42 @@ public final class GlucoseChartManager {
public var gestureRecognizer: UIGestureRecognizer?
/// The latest allowed date on the X-axis
public var maxEndDate = Date.distantFuture {
/// reference to coreDataManager
public var coreDataManager: CoreDataManager? {
didSet {
if maxEndDate != oldValue {
trace("New chart max end date: %@", log: self.log, type: .info, String(describing: maxEndDate))
if let coreDataManager = coreDataManager {
bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager)
}
endDate = min(endDate, maxEndDate)
}
}
/// chartpoint array with actually reading values
// MARK: - private properties
/// chartpoint array with glucose values and timestamp
///
/// Whenever glucoseChartPoints is assigned a new value, glucoseChart is set to nil
public var glucoseChartPoints: [ChartPoint] = [] {
private var glucoseChartPoints: [ChartPoint] = [] {
didSet {
glucoseChart = nil
if let lastDate = glucoseChartPoints.last?.x as? ChartAxisValueDate {
updateEndDate(lastDate.date)
}
}
}
// MARK: - private properties
/// for logging
private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryStatusChartsManager)
private let colors: ChartColorPalette
private let colors = ChartColorPalette(axisLine: ConstantsGlucoseChart.axisLineColor, axisLabel: ConstantsGlucoseChart.axisLabelColor, grid: ConstantsGlucoseChart.gridColor, glucoseTint: ConstantsGlucoseChart.glucoseTintColor)
private let chartSettings: ChartSettings
private let chartSettings: ChartSettings = {
var settings = ChartSettings()
settings.top = ConstantsGlucoseChart.top
settings.bottom = ConstantsGlucoseChart.bottom
settings.trailing = ConstantsGlucoseChart.trailing
settings.leading = ConstantsGlucoseChart.leading
settings.axisTitleLabelsToLabelsSpacing = ConstantsGlucoseChart.axisTitleLabelsToLabelsSpacing
settings.labelsToAxisSpacingX = ConstantsGlucoseChart.labelsToAxisSpacingX
settings.clipInnerFrame = false
return settings
}()
private let labelsWidthY = ConstantsGlucoseChart.yAxisLabelsWidth
@ -59,8 +60,6 @@ public final class GlucoseChartManager {
didSet {
if endDate != oldValue {
trace("New chart enddate: %@", log: self.log, type: .info, String(describing: endDate))
xAxisValues = nil
// current difference between end and startdate
@ -81,6 +80,7 @@ public final class GlucoseChartManager {
/// see https://github.com/i-schuetz/SwiftCharts/blob/ec538d027d6d4c64028d85f86d3d72fcda41c016/SwiftCharts/AxisValues/ChartAxisValue.swift#L12, is not meant to be instantiated
private var xAxisValues: [ChartAxisValue]? {
didSet {
if let xAxisValues = xAxisValues, xAxisValues.count > 1 {
xAxisModel = ChartAxisModel(axisValues: xAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(20))
} else {
@ -110,12 +110,12 @@ public final class GlucoseChartManager {
/// timeformatter for horizontal axis label
private let axisLabelTimeFormatter: DateFormatter
/// a BgReadingsAccessor
private var bgReadingsAccessor: BgReadingsAccessor?
// MARK: - intializer
public init(colors: ChartColorPalette, settings: ChartSettings) {
self.colors = colors
self.chartSettings = settings
init() {
chartLabelSettings = ChartLabelSettings(
font: .systemFont(ofSize: 14), // caption1, but hard-coded until axis can scale with type preference
@ -127,6 +127,7 @@ public final class GlucoseChartManager {
// initialize enddate
endDate = Date()
// intialize startdate, which is enddate minus a few hours
startDate = endDate.addingTimeInterval(.hours(-UserDefaults.standard.chartWidthInHours))
@ -135,29 +136,6 @@ public final class GlucoseChartManager {
}
convenience public init() {
self.init(
colors: ChartColorPalette(
axisLine: ConstantsGlucoseChart.axisLineColor,
axisLabel: ConstantsGlucoseChart.axisLabelColor,
grid: ConstantsGlucoseChart.gridColor,
glucoseTint: ConstantsGlucoseChart.glucoseTintColor
),
settings: {
var settings = ChartSettings()
settings.top = ConstantsGlucoseChart.top
settings.bottom = ConstantsGlucoseChart.bottom
settings.trailing = ConstantsGlucoseChart.trailing
settings.leading = ConstantsGlucoseChart.leading
settings.axisTitleLabelsToLabelsSpacing = ConstantsGlucoseChart.axisTitleLabelsToLabelsSpacing
settings.labelsToAxisSpacingX = ConstantsGlucoseChart.labelsToAxisSpacingX
settings.clipInnerFrame = false
return settings
}()
)
}
// MARK: - public functions
/// updates chart ?
@ -177,12 +155,49 @@ public final class GlucoseChartManager {
)
}
/*glucoseChartPoints.map {
debuglogging("scalar = " + $0.y.scalar.description)
}*/
}
/// updates the glucoseChartPoints array and calls completionHandler when finished, also chart is set to nil
/// - parameters:
/// - completionHandler will be called when finished
public func updateGlucoseChartPoints(completionHandler: @escaping () -> ()) {
guard let bgReadingsAccessor = bgReadingsAccessor else {
trace("in updateGlucoseChartPoints, bgReadingsAccessor, probably coreDataManager is not assigned, looks like a coding error", log: self.log, type: .error)
return
}
let queue = OperationQueue()
let operation = BlockOperation(block: {
// reset endDate
self.endDate = Date()
// get glucosePoints from coredata
let glucoseChartPoints = bgReadingsAccessor.getBgReadingOnPrivateManagedObjectContext(from: self.startDate, to: self.endDate).compactMap {
ChartPoint(bgReading: $0, formatter: self.chartPointDateFormatter, unitIsMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl)
}
//let glucosePoints = BgReadingsAccessor.get
DispatchQueue.main.async {
self.glucoseChartPoints = glucoseChartPoints
completionHandler()
}
})
queue.addOperation {
operation.start()
}
}
public func didReceiveMemoryWarning() {
trace("in didReceiveMemoryWarning, Purging chart data in response to memory warning", log: self.log, type: .error)
@ -193,28 +208,6 @@ public final class GlucoseChartManager {
}
/// Updates the endDate using a new candidate date
///
/// Dates are rounded up to the next hour.
///
/// - Parameter date: The new candidate date
public func updateEndDate(_ date: Date) {
if date > endDate {
var components = DateComponents()
components.minute = 0
endDate = min(
maxEndDate,
Calendar.current.nextDate(
after: date,
matching: components,
matchingPolicy: .strict,
direction: .forward
) ?? date
)
}
}
public func glucoseChartWithFrame(_ frame: CGRect) -> Chart? {
if let chart = glucoseChart, chart.frame != frame {
@ -343,7 +336,7 @@ public final class GlucoseChartManager {
/// first, for each int in mappingArray, we create a ChartAxisValueDate, which will have as date one of the hours, starting with the lower hour + 1 hour - we will create 5 in this example, starting with hour 08 (7 + 3600 seconds)
let startDateLower = startDate.toLowerHour()
var xAxisValues = mappingArray.map { ChartAxisValueDate(date: Date(timeInterval: Double($0)*3600, since: startDateLower), formatter: axisLabelTimeFormatter, labelSettings: chartLabelSettings) }
var xAxisValues: [ChartAxisValue] = mappingArray.map { ChartAxisValueDate(date: Date(timeInterval: Double($0)*3600, since: startDateLower), formatter: axisLabelTimeFormatter, labelSettings: chartLabelSettings) }
/// insert the start Date as first element, in this example 07:26
xAxisValues.insert(ChartAxisValueDate(date: startDate, formatter: axisLabelTimeFormatter, labelSettings: chartLabelSettings), at: 0)
@ -354,8 +347,9 @@ public final class GlucoseChartManager {
/// don't show the first and last hour, because this is usually not something like 13 but rather 13:26
xAxisValues.first?.hidden = true
xAxisValues.last?.hidden = true
self.xAxisValues = xAxisValues
}
}

View File

@ -3,7 +3,7 @@ import UIKit
import os
/// development as explained in cocoacasts.com https://cocoacasts.com/bring-your-own
final class CoreDataManager {
public final class CoreDataManager {
// MARK: - Type Aliases

View File

@ -39,14 +39,10 @@ final class RootViewController: UIViewController {
@IBAction func preSnoozeButtonAction(_ sender: UIButton) {
glucoseChartManager?.updateChart()
glucoseChartManager?.prerender()
chartOutlet.reloadChart()
/*
let alert = UIAlertController(title: "Info", message: "Unfortuantely, presnooze functionality is not yet implemented", actionHandler: nil)
self.present(alert, animated: true, completion: nil)
*/
}
/// outlet for label that shows how many minutes ago and so on
@ -146,7 +142,7 @@ final class RootViewController: UIViewController {
super.viewWillAppear(animated)
// viewWillAppear when user switches eg from Settings Tab to Home Tab - latest reading value needs to be shown on the view, and also update minutes ago etc.
updateLabels()
updateLabelsAndChart()
}
override func viewDidLoad() {
@ -159,20 +155,21 @@ final class RootViewController: UIViewController {
self.setupApplicationData()
// update label texts, minutes ago, diff and value
self.updateLabels()
self.updateLabelsAndChart()
// create transmitter based on UserDefaults
self.initializeCGMTransmitter()
self.glucoseChartManager?.prerender()
self.chartOutlet.chartGenerator = { [weak self] (frame) in
return self?.glucoseChartManager?.glucoseChartWithFrame(frame)?.view
}
// glucoseChartManager still needs the refernce to coreDataManager
self.glucoseChartManager?.coreDataManager = self.coreDataManager
// and now call again updateGlucoseChart, as readings can be fetched now from coreData
self.updateGlucoseChart()
})
// initialize glucoseChartManager
glucoseChartManager = GlucoseChartManager()
// Setup View
setupView()
@ -232,11 +229,16 @@ final class RootViewController: UIViewController {
}
// whenever app comes from-back to freground, updateLabels needs to be called
ApplicationManager.shared.addClosureToRunWhenAppWillEnterForeground(key: applicationManagerKeyUpdateLabels, closure: {self.updateLabels()})
ApplicationManager.shared.addClosureToRunWhenAppWillEnterForeground(key: applicationManagerKeyUpdateLabels, closure: {self.updateLabelsAndChart()})
// setup AVAudioSession
setupAVAudioSession()
// initialize chartGenerator in chartOutlet
self.chartOutlet.chartGenerator = { [weak self] (frame) in
return self?.glucoseChartManager?.glucoseChartWithFrame(frame)?.view
}
}
/// sets AVAudioSession category to AVAudioSession.Category.playback with option mixWithOthers and
@ -250,7 +252,7 @@ final class RootViewController: UIViewController {
}
}
// creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager
// creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager, nightScoutFollowManager, alertManager, healthKitManager, bgReadingSpeaker, m5StackManager
private func setupApplicationData() {
// if coreDataManager is nil then there's no reason to continue
@ -309,8 +311,6 @@ final class RootViewController: UIViewController {
// setup m5StackManager
m5StackManager = M5StackManager(coreDataManager: coreDataManager)
// setup glucoseChartManager
glucoseChartManager = GlucoseChartManager()
}
/// process new glucose data received from transmitter.
@ -391,7 +391,7 @@ final class RootViewController: UIViewController {
// update notification
createBgReadingNotification()
// update all text in first screen
updateLabels()
updateLabelsAndChart()
}
nightScoutUploadManager?.upload()
@ -483,6 +483,10 @@ final class RootViewController: UIViewController {
preSnoozeButtonOutlet.setTitle(Texts_HomeView.snoozeButton, for: .normal)
transmitterButtonOutlet.setTitle(Texts_HomeView.transmitter, for: .normal)
// at this moment, coreDataManager is not yet initialized, we're just calling here prerender and reloadChart to show the chart with x and y axis and gridlines, but without readings. The readings will be loaded once coreDataManager is setup, after which updateGlucoseChart() will be called, which will initiate loading of readings from coredata
self.glucoseChartManager?.prerender()
self.chartOutlet.reloadChart()
}
// MARK: - private helper functions
@ -496,6 +500,15 @@ final class RootViewController: UIViewController {
}
private func updateGlucoseChart() {
glucoseChartManager?.updateGlucoseChartPoints {
self.glucoseChartManager?.prerender()
self.chartOutlet.reloadChart()
}
}
/// will call cgmTransmitter.initiatePairing() - also sets timer, if no successful pairing within a few seconds, then info will be given to user asking to wait another few minutes
private func initiateTransmitterPairing() {
@ -532,7 +545,7 @@ final class RootViewController: UIViewController {
// check if timer already exists, if so invalidate it
invalidateUpdateLabelsTimer()
// now recreate, schedule and return
return Timer.scheduledTimer(timeInterval: ConstantsHomeView.updateHomeViewIntervalInSeconds, target: self, selector: #selector(self.updateLabels), userInfo: nil, repeats: true)
return Timer.scheduledTimer(timeInterval: ConstantsHomeView.updateHomeViewIntervalInSeconds, target: self, selector: #selector(self.updateLabelsAndChart), userInfo: nil, repeats: true)
}
// call scheduleUpdateLabelsTimer function now - as the function setupUpdateLabelsTimer is called from viewdidload, it will be called immediately after app launch
@ -627,7 +640,7 @@ final class RootViewController: UIViewController {
}
// update labels
self.updateLabels()
self.updateLabelsAndChart()
}
}
}
@ -825,13 +838,16 @@ final class RootViewController: UIViewController {
}
}
/// updates the homescreen
@objc private func updateLabels() {
/// updates the homescreen labels and chart
@objc private func updateLabelsAndChart() {
debuglogging("in updateLabelsAndChart 1")
// check that bgReadingsAccessor exists, otherwise return - this happens if updateLabels is called from viewDidload at app launch
guard let bgReadingsAccessor = bgReadingsAccessor else {return}
debuglogging("in updateLabelsAndChart 2")
// last reading and lateButOneReading variable definition - optional
var lastReading:BgReading?
var lastButOneReading:BgReading?
@ -889,6 +905,10 @@ final class RootViewController: UIViewController {
minutesLabelOutlet.text = ""
diffLabelOutlet.text = ""
}
// update chart
updateGlucoseChart()
}
/// when user clicks transmitter button, this will create and present the actionsheet, contents depend on type of transmitter and sensor status
@ -1522,7 +1542,7 @@ extension RootViewController:NightScoutFollowerDelegate {
createBgReadingNotification()
// update all text in first screen
updateLabels()
updateLabelsAndChart()
// check alerts
if let alertManager = alertManager {