Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */; };
DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CD2D355879000D2A63 /* LogEntry.swift */; };
DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; };
DDA9ACA62D6A66D000E6F1A9 /* ContactColorMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */; };
DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */; };
DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */; };
DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */; };
Expand Down Expand Up @@ -572,6 +573,7 @@
DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
DD9ED0CD2D355879000D2A63 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = "<group>"; };
DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorMode.swift; sourceTree = "<group>"; };
DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactIncludeOption.swift; sourceTree = "<group>"; };
DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactType.swift; sourceTree = "<group>"; };
DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkHeartbeatBluetoothDevice.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1017,6 +1019,7 @@
DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */,
DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */,
DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */,
DDA9ACA52D6A66C800E6F1A9 /* ContactColorMode.swift */,
DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */,
);
path = Contact;
Expand Down Expand Up @@ -2039,6 +2042,7 @@
DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */,
DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */,
DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */,
DDA9ACA62D6A66D000E6F1A9 /* ContactColorMode.swift in Sources */,
DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */,
DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */,
FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */,
Expand Down
37 changes: 37 additions & 0 deletions LoopFollow/Contact/ContactColorMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// LoopFollow
// ContactColorMode.swift

import UIKit

enum ContactColorMode: String, Codable, CaseIterable {
case staticColor = "Static"
case dynamic = "Dynamic"

var displayName: String {
switch self {
case .staticColor:
return "Static"
case .dynamic:
return "Dynamic (BG Range)"
}
}

/// Returns the appropriate text color based on the mode and BG value
func textColor(for bgValue: Double, staticColor: UIColor) -> UIColor {
switch self {
case .staticColor:
return staticColor
case .dynamic:
let highLine = Storage.shared.highLine.value
let lowLine = Storage.shared.lowLine.value

if bgValue >= highLine {
return .systemYellow
} else if bgValue <= lowLine {
return .systemRed
} else {
return .systemGreen
}
}
}
}
136 changes: 84 additions & 52 deletions LoopFollow/Contact/ContactImageUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ class ContactImageUpdater {
return ContactColorOption(rawValue: rawValue)?.uiColor ?? .white
}

func updateContactImage(bgValue: String, trend: String, delta: String, stale: Bool) {
private func textColor(for contactType: ContactType) -> UIColor {
guard contactType == .BG else { return savedTextUIColor }
let colorMode = Storage.shared.contactColorMode.value
// Use raw BG value in mg/dL (same units as highLine/lowLine)
let bgNumeric = Double(Observable.shared.bg.value ?? 0)
return colorMode.textColor(for: bgNumeric, staticColor: savedTextUIColor)
}

func updateContactImage(bgValue: String, trend: String, delta: String, iob: String, stale: Bool) {
queue.async {
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
LogManager.shared.log(category: .contact, message: "Access to contacts is not authorized.")
Expand All @@ -37,9 +45,17 @@ class ContactImageUpdater {
continue
}

if contactType == .IOB, Storage.shared.contactIOB.value != .separate {
continue
}

let contactName = "\(bundleDisplayName) - \(contactType.rawValue)"

guard let imageData = self.generateContactImage(bgValue: bgValue, trend: trend, delta: delta, stale: stale, contactType: contactType)?.pngData() else {
let includedFields = self.getIncludedFields(for: contactType)

let dynamicTextColor = self.textColor(for: contactType)

guard let imageData = self.generateContactImage(bgValue: bgValue, trend: trend, delta: delta, iob: iob, stale: stale, contactType: contactType, includedFields: includedFields, textColor: dynamicTextColor)?.pngData() else {
LogManager.shared.log(category: .contact, message: "Failed to generate contact image for \(contactName).")
continue
}
Expand Down Expand Up @@ -100,7 +116,24 @@ class ContactImageUpdater {
}
}

private func generateContactImage(bgValue: String, trend: String, delta: String, stale: Bool, contactType: ContactType) -> UIImage? {
private func getIncludedFields(for contactType: ContactType) -> [ContactType] {
var included: [ContactType] = []
if Storage.shared.contactTrend.value == .include,
Storage.shared.contactTrendTarget.value == contactType {
included.append(.Trend)
}
if Storage.shared.contactDelta.value == .include,
Storage.shared.contactDeltaTarget.value == contactType {
included.append(.Delta)
}
if Storage.shared.contactIOB.value == .include,
Storage.shared.contactIOBTarget.value == contactType {
included.append(.IOB)
}
return included
}

private func generateContactImage(bgValue: String, trend: String, delta: String, iob: String, stale: Bool, contactType: ContactType, includedFields: [ContactType], textColor: UIColor) -> UIImage? {
let size = CGSize(width: 300, height: 300)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
Expand All @@ -111,66 +144,65 @@ class ContactImageUpdater {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center

// Format extraDelta based on the user's unit preference
let unitPreference = Storage.shared.units.value
let yOffset: CGFloat = 48
if contactType == .Trend, Storage.shared.contactTrend.value == .separate {
let trendRect = CGRect(x: 0, y: 46, width: size.width, height: size.height - 80)
let trendFontSize = max(40, 200 - CGFloat(trend.count * 15))

let trendAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.boldSystemFont(ofSize: trendFontSize),
.foregroundColor: stale ? UIColor.gray : savedTextUIColor,
.paragraphStyle: paragraphStyle,
]
// Get the primary value for this contact type
let primaryValue: String
switch contactType {
case .BG: primaryValue = bgValue
case .Trend: primaryValue = trend
case .Delta: primaryValue = delta
case .IOB: primaryValue = iob
}

trend.draw(in: trendRect, withAttributes: trendAttributes)
} else if contactType == .Delta, Storage.shared.contactDelta.value == .separate {
let deltaRect = CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
let deltaFontSize = max(40, 200 - CGFloat(delta.count * 15))
// Build extra values from included fields
var extraValues: [String] = []
for field in includedFields {
switch field {
case .Trend: extraValues.append(trend)
case .Delta: extraValues.append(delta)
case .IOB: extraValues.append(iob)
case .BG: break
}
}

let deltaAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.boldSystemFont(ofSize: deltaFontSize),
.foregroundColor: stale ? UIColor.gray : savedTextUIColor,
.paragraphStyle: paragraphStyle,
]
let hasExtras = !extraValues.isEmpty

delta.draw(in: deltaRect, withAttributes: deltaAttributes)
} else if contactType == .BG {
let includesExtra = Storage.shared.contactDelta.value == .include || Storage.shared.contactTrend.value == .include
// Determine font sizes based on number of extras
let maxFontSize: CGFloat = extraValues.count >= 2 ? 140 : (hasExtras ? 160 : 200)
let extraFontSize: CGFloat = extraValues.count >= 2 ? 60 : 90

let maxFontSize: CGFloat = includesExtra ? 160 : 200
let fontSize = maxFontSize - CGFloat(bgValue.count * 15)
var bgAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.boldSystemFont(ofSize: fontSize),
.foregroundColor: stale ? UIColor.gray : savedTextUIColor,
.paragraphStyle: paragraphStyle,
]
let fontSize = max(40, maxFontSize - CGFloat(primaryValue.count * 15))

if stale {
// Force background color back to black if stale
UIColor.black.setFill()
context.fill(CGRect(origin: .zero, size: size))
bgAttributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
}
let isBGStale = stale && contactType == .BG

let bgRect: CGRect = includesExtra
? CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2)
: CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
var primaryAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.boldSystemFont(ofSize: fontSize),
.foregroundColor: isBGStale ? UIColor.gray : textColor,
.paragraphStyle: paragraphStyle,
]

bgValue.draw(in: bgRect, withAttributes: bgAttributes)
if isBGStale {
UIColor.black.setFill()
context.fill(CGRect(origin: .zero, size: size))
primaryAttributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
}

if includesExtra {
let extraRect = CGRect(x: 0, y: size.height / 2 + 6, width: size.width, height: size.height / 2 - 20)
let extraAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 90),
.foregroundColor: stale ? UIColor.gray : savedTextUIColor,
.paragraphStyle: paragraphStyle,
]
let primaryRect: CGRect = hasExtras
? CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2)
: CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)

let extra = Storage.shared.contactDelta.value == .include ? delta : trend
extra.draw(in: extraRect, withAttributes: extraAttributes)
}
primaryValue.draw(in: primaryRect, withAttributes: primaryAttributes)

if hasExtras {
let extraString = extraValues.joined(separator: " ")
let extraRect = CGRect(x: 0, y: size.height / 2 + 6, width: size.width, height: size.height / 2 - 20)
let extraAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: extraFontSize),
.foregroundColor: isBGStale ? UIColor.gray : textColor,
.paragraphStyle: paragraphStyle,
]
extraString.draw(in: extraRect, withAttributes: extraAttributes)
}

let image = UIGraphicsGetImageFromCurrentImageContext()
Expand Down
3 changes: 2 additions & 1 deletion LoopFollow/Contact/ContactType.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// LoopFollow
// ContactType.swift

enum ContactType: String, CaseIterable {
enum ContactType: String, CaseIterable, Codable {
case BG
case Trend
case Delta
case IOB
}
1 change: 1 addition & 0 deletions LoopFollow/Controllers/Nightscout/BGData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ extension MainViewController {
bgValue: Observable.shared.bgText.value,
trend: Observable.shared.directionText.value,
delta: Observable.shared.deltaText.value,
iob: Observable.shared.iobText.value,
stale: Observable.shared.bgStale.value
)
}
Expand Down
13 changes: 13 additions & 0 deletions LoopFollow/Controllers/Nightscout/DeviceStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ extension MainViewController {

// NS Device Status Response Processor
func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) {
let previousIOBText = Observable.shared.iobText.value
infoManager.clearInfoData(types: [.iob, .cob, .battery, .pump, .pumpBattery, .target, .isf, .carbRatio, .updated, .recBolus, .tdd])

// For Loop, clear the current override here - For Trio, it is handled using treatments
Expand Down Expand Up @@ -239,6 +240,18 @@ extension MainViewController {
// Mark device status as loaded for initial loading state
markDataLoaded("deviceStatus")

if Storage.shared.contactEnabled.value, Storage.shared.contactIOB.value != .off,
Observable.shared.iobText.value != previousIOBText
{
contactImageUpdater.updateContactImage(
bgValue: Observable.shared.bgText.value,
trend: Observable.shared.directionText.value,
delta: Observable.shared.deltaText.value,
iob: Observable.shared.iobText.value,
stale: Observable.shared.bgStale.value
)
}

LogManager.shared.log(category: .deviceStatus, message: "Update Device Status done", isDebug: true)
}
}
1 change: 1 addition & 0 deletions LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ extension MainViewController {
if let insulinMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") {
infoManager.updateInfoData(type: .iob, value: insulinMetric)
latestIOB = insulinMetric
Observable.shared.iobText.value = insulinMetric.formattedValue()
}

// COB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ extension MainViewController {
if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") {
infoManager.updateInfoData(type: .iob, value: iobMetric)
latestIOB = iobMetric
Observable.shared.iobText.value = iobMetric.formattedValue()
}

// COB
Expand Down
Loading