|
@@ -0,0 +1,865 @@
|
|
|
|
|
+import Foundation
|
|
|
|
|
+import SwiftUI
|
|
|
|
|
+
|
|
|
|
|
+struct ContactPicture: View {
|
|
|
|
|
+ private enum Config {
|
|
|
|
|
+ static let lag: TimeInterval = 30
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Binding var contact: ContactTrickEntry
|
|
|
|
|
+ @Binding var state: ContactTrickState
|
|
|
|
|
+
|
|
|
|
|
+ private static let formatter: DateFormatter = {
|
|
|
|
|
+ let formatter = DateFormatter()
|
|
|
|
|
+ formatter.dateFormat = "HH:mm"
|
|
|
|
|
+ return formatter
|
|
|
|
|
+ }()
|
|
|
|
|
+
|
|
|
|
|
+ static func getImage(
|
|
|
|
|
+ contact: ContactTrickEntry,
|
|
|
|
|
+ state: ContactTrickState
|
|
|
|
|
+ ) -> UIImage {
|
|
|
|
|
+ let width = 1024.0
|
|
|
|
|
+ let height = 1024.0
|
|
|
|
|
+ var rect = CGRect(x: 0, y: 0, width: width, height: height)
|
|
|
|
|
+ let textColor: Color = contact.darkMode ?
|
|
|
|
|
+ Color(red: 250 / 256, green: 250 / 256, blue: 250 / 256) :
|
|
|
|
|
+ Color(red: 20 / 256, green: 20 / 256, blue: 20 / 256)
|
|
|
|
|
+ let secondaryTextColor: Color = contact.darkMode ?
|
|
|
|
|
+ Color(red: 220 / 256, green: 220 / 256, blue: 220 / 256) :
|
|
|
|
|
+ Color(red: 40 / 256, green: 40 / 256, blue: 40 / 256)
|
|
|
|
|
+ let fontWeight = contact.fontWeight
|
|
|
|
|
+
|
|
|
|
|
+ UIGraphicsBeginImageContext(rect.size)
|
|
|
|
|
+ if let context = UIGraphicsGetCurrentContext() {
|
|
|
|
|
+ context.setShouldAntialias(true)
|
|
|
|
|
+ context.setAllowsAntialiasing(true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let ringWidth = Double(contact.ringWidth.rawValue) / 100.0
|
|
|
|
|
+ let ringGap = Double(contact.ringGap.rawValue) / 100.0
|
|
|
|
|
+ let outerGap = 0.03
|
|
|
|
|
+
|
|
|
|
|
+ if contact.ring != .none {
|
|
|
|
|
+ rect = CGRect(
|
|
|
|
|
+ x: rect.minX + width * outerGap,
|
|
|
|
|
+ y: rect.minY + height * outerGap,
|
|
|
|
|
+ width: rect.width - width * outerGap * 2,
|
|
|
|
|
+ height: rect.height - height * outerGap * 2
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ let ringRect = CGRect(
|
|
|
|
|
+ x: rect.minX + width * ringWidth * 0.5,
|
|
|
|
|
+ y: rect.minY + height * ringWidth * 0.5,
|
|
|
|
|
+ width: rect.width - width * ringWidth,
|
|
|
|
|
+ height: rect.height - height * ringWidth
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ drawRing(ring: contact.ring, contact: contact, state: state, rect: ringRect, strokeWidth: width * ringWidth)
|
|
|
|
|
+
|
|
|
|
|
+ rect = CGRect(
|
|
|
|
|
+ x: rect.minX + width * (ringWidth + ringGap),
|
|
|
|
|
+ y: rect.minY + height * (ringWidth + ringGap),
|
|
|
|
|
+ width: rect.width - width * (ringWidth + ringGap) * 2,
|
|
|
|
|
+ height: rect.height - height * (ringWidth + ringGap) * 2
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch contact.layout {
|
|
|
|
|
+ case .single:
|
|
|
|
|
+ let showTop = contact.top != .none
|
|
|
|
|
+ let showBottom = contact.bottom != .none
|
|
|
|
|
+
|
|
|
|
|
+ let centerX = rect.minX + rect.width / 2
|
|
|
|
|
+ let centerY = rect.minY + rect.height / 2
|
|
|
|
|
+ let radius = min(rect.width, rect.height) / 2
|
|
|
|
|
+
|
|
|
|
|
+ var primaryHeight = radius * 0.8
|
|
|
|
|
+ let topHeight = radius * 0.5
|
|
|
|
|
+ var bottomHeight = radius * 0.5
|
|
|
|
|
+
|
|
|
|
|
+ var primaryY = centerY - primaryHeight / 2
|
|
|
|
|
+
|
|
|
|
|
+ if contact.bottom == .none, contact.top != .none {
|
|
|
|
|
+ primaryY += radius * 0.2
|
|
|
|
|
+ }
|
|
|
|
|
+ if contact.bottom != .none, contact.top == .none {
|
|
|
|
|
+ primaryY -= radius * 0.2
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let topY = primaryY - topHeight
|
|
|
|
|
+ var bottomY = primaryY + primaryHeight
|
|
|
|
|
+
|
|
|
|
|
+ let primaryWidth = 2 * sqrt(radius * radius - (primaryHeight * 0.5) * (primaryHeight * 0.5))
|
|
|
|
|
+ let topWidth = 2 *
|
|
|
|
|
+ sqrt(radius * radius - (topHeight + primaryHeight * 0.5) * (topHeight + primaryHeight * 0.5))
|
|
|
|
|
+ var bottomWidth = 2 *
|
|
|
|
|
+ sqrt(radius * radius - (bottomHeight + primaryHeight * 0.5) * (bottomHeight + primaryHeight * 0.5))
|
|
|
|
|
+
|
|
|
|
|
+ if contact.bottom != .none, contact.top == .none {
|
|
|
|
|
+ // move things around a little bit to give more space to the bottom area
|
|
|
|
|
+ if contact.ring == .iob || contact.ring == .cob || contact.ring == .iobcob ||
|
|
|
|
|
+ (contact.bottom == .trend && contact.ring == .loop)
|
|
|
|
|
+ {
|
|
|
|
|
+ bottomHeight = bottomHeight + height * ringWidth * 2
|
|
|
|
|
+ bottomWidth = bottomWidth + width * ringWidth * 2
|
|
|
|
|
+ } else if contact.ring == .loop {
|
|
|
|
|
+ primaryHeight = primaryHeight - height * ringWidth
|
|
|
|
|
+ bottomY = primaryY + primaryHeight
|
|
|
|
|
+ bottomHeight = bottomHeight + height * ringWidth * 2
|
|
|
|
|
+ bottomWidth = bottomWidth + width * ringWidth * 2
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let primaryRect = (showTop || showBottom) ? CGRect(
|
|
|
|
|
+ x: centerX - primaryWidth * 0.5,
|
|
|
|
|
+ y: primaryY,
|
|
|
|
|
+ width: primaryWidth,
|
|
|
|
|
+ height: primaryHeight
|
|
|
|
|
+ ) : rect
|
|
|
|
|
+ let topRect = CGRect(
|
|
|
|
|
+ x: centerX - topWidth * 0.5,
|
|
|
|
|
+ y: topY,
|
|
|
|
|
+ width: topWidth,
|
|
|
|
|
+ height: topHeight
|
|
|
|
|
+ )
|
|
|
|
|
+ let bottomRect = CGRect(
|
|
|
|
|
+ x: centerX - bottomWidth * 0.5,
|
|
|
|
|
+ y: bottomY,
|
|
|
|
|
+ width: bottomWidth,
|
|
|
|
|
+ height: bottomHeight
|
|
|
|
|
+ )
|
|
|
|
|
+ let secondaryFontSize = contact.secondaryFontSize
|
|
|
|
|
+
|
|
|
|
|
+ displayPiece(
|
|
|
|
|
+ value: contact.primary,
|
|
|
|
|
+ contact: contact,
|
|
|
|
|
+ state: state,
|
|
|
|
|
+ rect: primaryRect,
|
|
|
|
|
+ fitHeigh: false,
|
|
|
|
|
+ fontSize: contact.fontSize.rawValue,
|
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
|
+ fontWidth: contact.fontWidth,
|
|
|
|
|
+ color: textColor
|
|
|
|
|
+ )
|
|
|
|
|
+ if showTop {
|
|
|
|
|
+ displayPiece(
|
|
|
|
|
+ value: contact.top,
|
|
|
|
|
+ contact: contact,
|
|
|
|
|
+ state: state,
|
|
|
|
|
+ rect: topRect,
|
|
|
|
|
+ fitHeigh: true,
|
|
|
|
|
+ fontSize: secondaryFontSize.rawValue,
|
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
|
+ fontWidth: contact.fontWidth,
|
|
|
|
|
+ color: secondaryTextColor
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ if showBottom {
|
|
|
|
|
+ displayPiece(
|
|
|
|
|
+ value: contact.bottom,
|
|
|
|
|
+ contact: contact,
|
|
|
|
|
+ state: state,
|
|
|
|
|
+ rect: bottomRect,
|
|
|
|
|
+ fitHeigh: true,
|
|
|
|
|
+ fontSize: secondaryFontSize.rawValue,
|
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
|
+ fontWidth: contact.fontWidth,
|
|
|
|
|
+ color: secondaryTextColor
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ case .split:
|
|
|
|
|
+ let centerX = rect.origin.x + rect.size.width / 2
|
|
|
|
|
+ let centerY = rect.origin.y + rect.size.height / 2
|
|
|
|
|
+ let radius = min(rect.size.width, rect.size.height) / 2
|
|
|
|
|
+
|
|
|
|
|
+ let rectangleHeight = radius * sqrt(2) / 2
|
|
|
|
|
+ let rectangleWidth = sqrt(2) * radius
|
|
|
|
|
+
|
|
|
|
|
+ let topY = centerY - rectangleHeight
|
|
|
|
|
+ let bottomY = centerY
|
|
|
|
|
+
|
|
|
|
|
+ let topRect = CGRect(
|
|
|
|
|
+ x: centerX - rectangleWidth / 2,
|
|
|
|
|
+ y: topY,
|
|
|
|
|
+ width: rectangleWidth,
|
|
|
|
|
+ height: rectangleHeight
|
|
|
|
|
+ )
|
|
|
|
|
+ let bottomRect = CGRect(
|
|
|
|
|
+ x: centerX - rectangleWidth / 2,
|
|
|
|
|
+ y: bottomY,
|
|
|
|
|
+ width: rectangleWidth,
|
|
|
|
|
+ height: rectangleHeight
|
|
|
|
|
+ )
|
|
|
|
|
+ let topFontSize = contact.fontSize
|
|
|
|
|
+ let bottomFontSize = contact.secondaryFontSize
|
|
|
|
|
+
|
|
|
|
|
+ displayPiece(
|
|
|
|
|
+ value: contact.top,
|
|
|
|
|
+ contact: contact,
|
|
|
|
|
+ state: state,
|
|
|
|
|
+ rect: topRect,
|
|
|
|
|
+ fitHeigh: true,
|
|
|
|
|
+ fontSize: topFontSize.rawValue,
|
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
|
+ fontWidth: contact.fontWidth,
|
|
|
|
|
+ color: textColor
|
|
|
|
|
+ )
|
|
|
|
|
+ displayPiece(
|
|
|
|
|
+ value: contact.bottom,
|
|
|
|
|
+ contact: contact,
|
|
|
|
|
+ state: state,
|
|
|
|
|
+ rect: bottomRect,
|
|
|
|
|
+ fitHeigh: true,
|
|
|
|
|
+ fontSize: bottomFontSize.rawValue,
|
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
|
+ fontWidth: contact.fontWidth,
|
|
|
|
|
+ color: textColor
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
|
|
|
+ UIGraphicsEndImageContext()
|
|
|
|
|
+ return image ?? UIImage()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func displayPiece(
|
|
|
|
|
+ value: ContactTrickValue,
|
|
|
|
|
+ contact: ContactTrickEntry,
|
|
|
|
|
+ state: ContactTrickState,
|
|
|
|
|
+ rect: CGRect,
|
|
|
|
|
+ fitHeigh: Bool,
|
|
|
|
|
+ fontSize: Int,
|
|
|
|
|
+ fontWeight: Font.Weight,
|
|
|
|
|
+ fontWidth: Font.Width,
|
|
|
|
|
+ color: Color
|
|
|
|
|
+ ) {
|
|
|
|
|
+ guard value != .none else { return }
|
|
|
|
|
+ if value == .ring {
|
|
|
|
|
+ drawRing(
|
|
|
|
|
+ ring: .loop,
|
|
|
|
|
+ contact: contact,
|
|
|
|
|
+ state: state,
|
|
|
|
|
+ rect: CGRect(
|
|
|
|
|
+ x: rect.minX + rect.width * 0.10,
|
|
|
|
|
+ y: rect.minY + rect.height * 0.10,
|
|
|
|
|
+ width: rect.width * 0.80,
|
|
|
|
|
+ height: rect.height * 0.80
|
|
|
|
|
+ ),
|
|
|
|
|
+ strokeWidth: 10.0
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ let text: String? = switch value {
|
|
|
|
|
+ case .glucose: state.glucose
|
|
|
|
|
+ case .eventualBG: state.eventualBG
|
|
|
|
|
+ case .delta: state.delta
|
|
|
|
|
+ case .trend: state.trend
|
|
|
|
|
+ case .lastLoopDate: state.lastLoopDate.map({ formatter.string(from: $0) })
|
|
|
|
|
+ case .cob: state.cobText
|
|
|
|
|
+ case .iob: state.iobText
|
|
|
|
|
+ default: nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let textColor: Color = switch value {
|
|
|
|
|
+ case .cob: .loopYellow
|
|
|
|
|
+ default: color
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if let text = text {
|
|
|
|
|
+ drawText(
|
|
|
|
|
+ text: text,
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ fitHeigh: fitHeigh,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
|
+ fontWidth: fontWidth,
|
|
|
|
|
+ color: textColor
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func drawText(
|
|
|
|
|
+ text: String,
|
|
|
|
|
+ rect: CGRect,
|
|
|
|
|
+ fitHeigh: Bool,
|
|
|
|
|
+ fontSize: Int,
|
|
|
|
|
+ fontWeight: Font.Weight,
|
|
|
|
|
+ fontWidth: Font.Width,
|
|
|
|
|
+ color: Color
|
|
|
|
|
+ ) {
|
|
|
|
|
+ var theFontSize = fontSize
|
|
|
|
|
+
|
|
|
|
|
+ func makeAttributes(_ size: Int) -> [NSAttributedString.Key: Any] {
|
|
|
|
|
+ let font = UIFont.systemFont(ofSize: CGFloat(size), weight: fontWeight.uiFontWeight)
|
|
|
|
|
+ return [
|
|
|
|
|
+ .font: font,
|
|
|
|
|
+ .foregroundColor: UIColor(color),
|
|
|
|
|
+ .kern: fontWidth.value * Double(fontSize) // `kern` is the correct key for tracking
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var attributes: [NSAttributedString.Key: Any] = makeAttributes(theFontSize)
|
|
|
|
|
+
|
|
|
|
|
+ var stringSize = text.size(withAttributes: attributes)
|
|
|
|
|
+ while stringSize.width > rect.width * 0.90 || fitHeigh && (stringSize.height > rect.height * 0.95), theFontSize > 50 {
|
|
|
|
|
+ theFontSize -= 10
|
|
|
|
|
+ attributes = makeAttributes(theFontSize)
|
|
|
|
|
+ stringSize = text.size(withAttributes: attributes)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ text.draw(
|
|
|
|
|
+ in: CGRect(
|
|
|
|
|
+ x: rect.minX + (rect.width - stringSize.width) / 2,
|
|
|
|
|
+ y: rect.minY + (rect.height - stringSize.height) / 2,
|
|
|
|
|
+ width: stringSize.width,
|
|
|
|
|
+ height: stringSize.height
|
|
|
|
|
+ ),
|
|
|
|
|
+ withAttributes: attributes
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func drawRing(
|
|
|
|
|
+ ring: ContactTrickLargeRing,
|
|
|
|
|
+ contact: ContactTrickEntry,
|
|
|
|
|
+ state: ContactTrickState,
|
|
|
|
|
+ rect: CGRect,
|
|
|
|
|
+ strokeWidth: Double
|
|
|
|
|
+ ) {
|
|
|
|
|
+ guard let context = UIGraphicsGetCurrentContext() else {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ switch ring {
|
|
|
|
|
+ case .loop:
|
|
|
|
|
+ let color = ringColor(contact: contact, state: state)
|
|
|
|
|
+
|
|
|
|
|
+ let strokeWidth = strokeWidth
|
|
|
|
|
+ let center = CGPoint(x: rect.midX, y: rect.midY)
|
|
|
|
|
+ let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2
|
|
|
|
|
+
|
|
|
|
|
+ context.setLineWidth(strokeWidth)
|
|
|
|
|
+ context.setStrokeColor(UIColor(color).cgColor)
|
|
|
|
|
+
|
|
|
|
|
+ context.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: false)
|
|
|
|
|
+
|
|
|
|
|
+ context.strokePath()
|
|
|
|
|
+ case .iob:
|
|
|
|
|
+ if let iob = state.iob, state.maxIOB > 0.1 {
|
|
|
|
|
+ drawProgressBar(
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ progress: Double(iob) / Double(state.maxIOB),
|
|
|
|
|
+ colors: [contact.darkMode ? .blue : .blue, contact.darkMode ? .pink : .red],
|
|
|
|
|
+ strokeWidth: strokeWidth
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ case .cob:
|
|
|
|
|
+ if let cob = state.cob, state.maxCOB > 0.01 {
|
|
|
|
|
+ drawProgressBar(
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ progress: Double(cob) / Double(state.maxCOB),
|
|
|
|
|
+ colors: [.loopYellow, .red],
|
|
|
|
|
+ strokeWidth: strokeWidth
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ case .iobcob:
|
|
|
|
|
+ if state.maxIOB > 0.01, state.maxCOB > 0.01 {
|
|
|
|
|
+ drawDoubleProgressBar(
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ progress1: state.iob.map { Double($0) / Double(state.maxIOB) },
|
|
|
|
|
+ progress2: state.cob.map { Double($0) / Double(state.maxCOB) },
|
|
|
|
|
+ colors1: [contact.darkMode ? .blue : .blue, contact.darkMode ? .pink : .red],
|
|
|
|
|
+ colors2: [.loopYellow, .red],
|
|
|
|
|
+ strokeWidth: strokeWidth
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ default:
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func drawProgressBar(
|
|
|
|
|
+ rect: CGRect,
|
|
|
|
|
+ progress: Double,
|
|
|
|
|
+ colors: [Color],
|
|
|
|
|
+ strokeWidth: Double
|
|
|
|
|
+ ) {
|
|
|
|
|
+ let startAngle: CGFloat = -(.pi + .pi / 4.0)
|
|
|
|
|
+ let endAngle: CGFloat = .pi / 4.0
|
|
|
|
|
+
|
|
|
|
|
+ drawGradientArc(
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ progress: progress,
|
|
|
|
|
+ colors: colors,
|
|
|
|
|
+ strokeWidth: strokeWidth,
|
|
|
|
|
+ startAngle: startAngle,
|
|
|
|
|
+ endAngle: endAngle,
|
|
|
|
|
+ gradientDirection: .leftToRight
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func drawDoubleProgressBar(
|
|
|
|
|
+ rect: CGRect,
|
|
|
|
|
+ progress1: Double?,
|
|
|
|
|
+ progress2: Double?,
|
|
|
|
|
+ colors1: [Color],
|
|
|
|
|
+ colors2: [Color],
|
|
|
|
|
+ strokeWidth: Double
|
|
|
|
|
+ ) {
|
|
|
|
|
+ if let progress1 = progress1 {
|
|
|
|
|
+ let startAngle1: CGFloat = .pi / 2 + .pi / 5
|
|
|
|
|
+ let endAngle1: CGFloat = 3 * .pi / 2 - .pi / 5
|
|
|
|
|
+ drawGradientArc(
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ progress: progress1,
|
|
|
|
|
+ colors: colors1,
|
|
|
|
|
+ strokeWidth: strokeWidth,
|
|
|
|
|
+ startAngle: startAngle1,
|
|
|
|
|
+ endAngle: endAngle1,
|
|
|
|
|
+ gradientDirection: .bottomToTop
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ if let progress2 = progress2 {
|
|
|
|
|
+ let startAngle2: CGFloat = .pi / 2 - .pi / 5
|
|
|
|
|
+ let endAngle2: CGFloat = -.pi / 2 + .pi / 5
|
|
|
|
|
+ drawGradientArc(
|
|
|
|
|
+ rect: rect,
|
|
|
|
|
+ progress: progress2,
|
|
|
|
|
+ colors: colors2,
|
|
|
|
|
+ strokeWidth: strokeWidth,
|
|
|
|
|
+ startAngle: startAngle2,
|
|
|
|
|
+ endAngle: endAngle2,
|
|
|
|
|
+ gradientDirection: .bottomToTop
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func drawGradientArc(
|
|
|
|
|
+ rect: CGRect,
|
|
|
|
|
+ progress: Double,
|
|
|
|
|
+ colors: [Color],
|
|
|
|
|
+ strokeWidth: Double,
|
|
|
|
|
+ startAngle: Double,
|
|
|
|
|
+ endAngle: Double,
|
|
|
|
|
+ gradientDirection: GradientDirection
|
|
|
|
|
+ ) {
|
|
|
|
|
+ guard let context = UIGraphicsGetCurrentContext() else {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let colors = colors.map { c in UIColor(c).cgColor }
|
|
|
|
|
+ let locations: [CGFloat] = [0.0, 1.0]
|
|
|
|
|
+ guard let gradient = CGGradient(
|
|
|
|
|
+ colorsSpace: CGColorSpaceCreateDeviceRGB(),
|
|
|
|
|
+ colors: colors as CFArray,
|
|
|
|
|
+ locations: locations
|
|
|
|
|
+ ) else {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ context.saveGState()
|
|
|
|
|
+
|
|
|
|
|
+ let center = CGPoint(x: rect.midX, y: rect.midY)
|
|
|
|
|
+ let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2
|
|
|
|
|
+
|
|
|
|
|
+ // angle - The angle to the starting point of the arc, measured in radians from the positive x-axis.
|
|
|
|
|
+
|
|
|
|
|
+ context.setLineWidth(strokeWidth)
|
|
|
|
|
+ context.setLineCap(.round)
|
|
|
|
|
+
|
|
|
|
|
+ let circumference = 2 * .pi * radius
|
|
|
|
|
+ let offsetAngle = (strokeWidth / circumference * 1.1) * 2 * .pi
|
|
|
|
|
+
|
|
|
|
|
+ let (start, middle, end) = if startAngle > endAngle {
|
|
|
|
|
+ (
|
|
|
|
|
+ endAngle,
|
|
|
|
|
+ startAngle - (startAngle - endAngle) * max(min(progress, 1.0), 0.0),
|
|
|
|
|
+ startAngle
|
|
|
|
|
+ )
|
|
|
|
|
+ } else {
|
|
|
|
|
+ (
|
|
|
|
|
+ startAngle,
|
|
|
|
|
+ startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0),
|
|
|
|
|
+ endAngle
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if start < middle - offsetAngle {
|
|
|
|
|
+ let arcPath1 = UIBezierPath()
|
|
|
|
|
+ arcPath1.addArc(
|
|
|
|
|
+ withCenter: center,
|
|
|
|
|
+ radius: radius,
|
|
|
|
|
+ startAngle: start,
|
|
|
|
|
+ endAngle: middle - offsetAngle,
|
|
|
|
|
+ clockwise: true
|
|
|
|
|
+ )
|
|
|
|
|
+ context.addPath(arcPath1.cgPath)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if middle + offsetAngle < end {
|
|
|
|
|
+ let arcPath2 = UIBezierPath()
|
|
|
|
|
+ arcPath2.addArc(
|
|
|
|
|
+ withCenter: center,
|
|
|
|
|
+ radius: radius,
|
|
|
|
|
+ startAngle: middle + offsetAngle,
|
|
|
|
|
+ endAngle: end,
|
|
|
|
|
+ clockwise: true
|
|
|
|
|
+ )
|
|
|
|
|
+ context.addPath(arcPath2.cgPath)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ context.replacePathWithStrokedPath()
|
|
|
|
|
+ context.clip()
|
|
|
|
|
+
|
|
|
|
|
+ switch gradientDirection {
|
|
|
|
|
+ case .bottomToTop:
|
|
|
|
|
+ context.drawLinearGradient(
|
|
|
|
|
+ gradient,
|
|
|
|
|
+ start: CGPoint(x: rect.midX, y: rect.maxY),
|
|
|
|
|
+ end: CGPoint(x: rect.midX, y: rect.minY),
|
|
|
|
|
+ options: []
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ case .leftToRight:
|
|
|
|
|
+ context.drawLinearGradient(
|
|
|
|
|
+ gradient,
|
|
|
|
|
+ start: CGPoint(x: rect.minX, y: rect.midY),
|
|
|
|
|
+ end: CGPoint(x: rect.maxX, y: rect.midY),
|
|
|
|
|
+ options: []
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ context.resetClip()
|
|
|
|
|
+
|
|
|
|
|
+ let circleCenter = CGPoint(
|
|
|
|
|
+ x: center.x + radius * cos(middle),
|
|
|
|
|
+ y: center.y + radius * sin(middle)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ context.setLineWidth(strokeWidth * 0.7)
|
|
|
|
|
+ context.setStrokeColor(UIColor.white.cgColor)
|
|
|
|
|
+ context.addArc(
|
|
|
|
|
+ center: circleCenter,
|
|
|
|
|
+ radius: 0,
|
|
|
|
|
+ startAngle: 0,
|
|
|
|
|
+ endAngle: .pi * 2,
|
|
|
|
|
+ clockwise: true
|
|
|
|
|
+ )
|
|
|
|
|
+ context.strokePath()
|
|
|
|
|
+
|
|
|
|
|
+ context.restoreGState()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func ringColor(
|
|
|
|
|
+ contact _: ContactTrickEntry,
|
|
|
|
|
+ state: ContactTrickState
|
|
|
|
|
+ ) -> Color {
|
|
|
|
|
+ guard let lastLoopDate = state.lastLoopDate else {
|
|
|
|
|
+ return .loopGray
|
|
|
|
|
+ }
|
|
|
|
|
+ let delta = Date().timeIntervalSince(lastLoopDate) - Config.lag
|
|
|
|
|
+
|
|
|
|
|
+ if delta <= 5.minutes.timeInterval {
|
|
|
|
|
+ return .loopGreen
|
|
|
|
|
+ } else if delta <= 10.minutes.timeInterval {
|
|
|
|
|
+ return .loopYellow
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return .loopRed
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var uiImage: UIImage {
|
|
|
|
|
+ ContactPicture.getImage(contact: contact, state: state)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ Image(uiImage: uiImage)
|
|
|
|
|
+ .frame(width: 256, height: 256)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+extension Font.Weight {
|
|
|
|
|
+ var uiFontWeight: UIFont.Weight {
|
|
|
|
|
+ switch self {
|
|
|
|
|
+ case .ultraLight: return .ultraLight
|
|
|
|
|
+ case .thin: return .thin
|
|
|
|
|
+ case .light: return .light
|
|
|
|
|
+ case .regular: return .regular
|
|
|
|
|
+ case .medium: return .medium
|
|
|
|
|
+ case .semibold: return .semibold
|
|
|
|
|
+ case .bold: return .bold
|
|
|
|
|
+ case .heavy: return .heavy
|
|
|
|
|
+ case .black: return .black
|
|
|
|
|
+ default: return .regular
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+enum GradientDirection: Int {
|
|
|
|
|
+ case leftToRight
|
|
|
|
|
+ case bottomToTop
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+struct ContactPicturePreview: View {
|
|
|
|
|
+ @Binding var contact: ContactTrickEntry
|
|
|
|
|
+ @Binding var state: ContactTrickState
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ ContactPicture(contact: $contact, state: $state)
|
|
|
|
|
+ Circle()
|
|
|
|
|
+ .stroke(lineWidth: 20)
|
|
|
|
|
+ .foregroundColor(.white)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(width: 256, height: 256)
|
|
|
|
|
+ .clipShape(Circle())
|
|
|
|
|
+ .preferredColorScheme($contact.wrappedValue.darkMode ? .dark : .light)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+struct ContactPicture_Previews: PreviewProvider {
|
|
|
|
|
+ struct Preview: View {
|
|
|
|
|
+ @State var rangeIndicator: Bool = true
|
|
|
|
|
+ @State var darkMode: Bool = true
|
|
|
|
|
+ @State var fontSize: ContactTrickEntry.FontSize = .small
|
|
|
|
|
+ @State var fontWeight: UIFont.Weight = .bold
|
|
|
|
|
+ @State var fontName: String? = "AmericanTypewriter"
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ top: .delta,
|
|
|
|
|
+ bottom: .trend,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ trend: "↗︎",
|
|
|
|
|
+ delta: "+0.2",
|
|
|
|
|
+ cob: 25,
|
|
|
|
|
+ cobText: "25"
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("bg + trend + delta")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ ring: .iob,
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ bottom: .trend,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ trend: "↗︎",
|
|
|
|
|
+ iob: 6.1,
|
|
|
|
|
+ iobText: "6.1",
|
|
|
|
|
+ maxIOB: 8.0
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("bg + trend + iob ring")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ top: .ring,
|
|
|
|
|
+ bottom: .trend,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ trend: "↗︎",
|
|
|
|
|
+ lastLoopDate: .now
|
|
|
|
|
+ ))
|
|
|
|
|
+
|
|
|
|
|
+ ).previewDisplayName("bg + trend + ring")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ ring: .loop,
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ top: .none,
|
|
|
|
|
+ bottom: .trend,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "8.8",
|
|
|
|
|
+ trend: "→",
|
|
|
|
|
+ lastLoopDate: .now
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("bg + trend + ring")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ ring: .loop,
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ top: .none,
|
|
|
|
|
+ bottom: .eventualBG,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ lastLoopDate: .now - 7.minutes,
|
|
|
|
|
+ eventualBG: "6.2"
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("bg + eventual + ring")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ ring: .loop,
|
|
|
|
|
+ primary: .lastLoopDate,
|
|
|
|
|
+ top: .none,
|
|
|
|
|
+ bottom: .none,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ trend: "↗︎",
|
|
|
|
|
+ lastLoopDate: .now - 2.minutes
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("lastLoopDate + ring")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ ring: .loop,
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ top: .none,
|
|
|
|
|
+ bottom: .none,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ lastLoopDate: .now,
|
|
|
|
|
+ iob: 6.1,
|
|
|
|
|
+ iobText: "6.1",
|
|
|
|
|
+ maxIOB: 8.0
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("bg + ring + ring2")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ layout: .split,
|
|
|
|
|
+ top: .iob,
|
|
|
|
|
+ bottom: .cob,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ iob: 1.5,
|
|
|
|
|
+ iobText: "1.5",
|
|
|
|
|
+ cob: 25,
|
|
|
|
|
+ cobText: "25"
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("iob + cob")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ layout: .single,
|
|
|
|
|
+ ring: .iobcob,
|
|
|
|
|
+ primary: .none,
|
|
|
|
|
+ ringWidth: .regular,
|
|
|
|
|
+ ringGap: .regular,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ iob: 1,
|
|
|
|
|
+ iobText: "5.5",
|
|
|
|
|
+ cob: 25,
|
|
|
|
|
+ cobText: "25",
|
|
|
|
|
+ maxIOB: 10,
|
|
|
|
|
+ maxCOB: 120
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("iobcob ring")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ layout: .single,
|
|
|
|
|
+ ring: .iobcob,
|
|
|
|
|
+ primary: .none,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ iob: -0.2,
|
|
|
|
|
+ iobText: "0.0",
|
|
|
|
|
+ cob: 0,
|
|
|
|
|
+ cobText: "0",
|
|
|
|
|
+ maxIOB: 10,
|
|
|
|
|
+ maxCOB: 120
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("iobcob ring (0/0)")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ layout: .single,
|
|
|
|
|
+ ring: .iobcob,
|
|
|
|
|
+ primary: .none,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ iob: 10,
|
|
|
|
|
+ iobText: "0.0",
|
|
|
|
|
+ cob: 120,
|
|
|
|
|
+ cobText: "0",
|
|
|
|
|
+ maxIOB: 10,
|
|
|
|
|
+ maxCOB: 120
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("iobcob ring (max/max)")
|
|
|
|
|
+
|
|
|
|
|
+ ContactPicturePreview(
|
|
|
|
|
+ contact: .constant(
|
|
|
|
|
+ ContactTrickEntry(
|
|
|
|
|
+ layout: .single,
|
|
|
|
|
+ ring: .iobcob,
|
|
|
|
|
+ primary: .glucose,
|
|
|
|
|
+ bottom: .trend,
|
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
|
+ fontWeight: .medium
|
|
|
|
|
+ )
|
|
|
|
|
+ ),
|
|
|
|
|
+ state: .constant(ContactTrickState(
|
|
|
|
|
+ glucose: "6.8",
|
|
|
|
|
+ trend: "↗︎",
|
|
|
|
|
+ iob: 5.5,
|
|
|
|
|
+ iobText: "5.5",
|
|
|
|
|
+ cob: 25,
|
|
|
|
|
+ cobText: "25",
|
|
|
|
|
+ maxIOB: 10,
|
|
|
|
|
+ maxCOB: 120
|
|
|
|
|
+ ))
|
|
|
|
|
+ ).previewDisplayName("bg + trend + iobcob ring")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var previews: some View {
|
|
|
|
|
+ Preview()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|