ContactPicture.swift 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. import Foundation
  2. import SwiftUI
  3. struct ContactPicture: View {
  4. private enum Config {
  5. static let lag: TimeInterval = 30
  6. }
  7. @Binding var contact: ContactImageEntry
  8. @Binding var state: ContactImageState
  9. private static let formatter: DateFormatter = {
  10. let formatter = DateFormatter()
  11. formatter.dateFormat = "HH:mm"
  12. return formatter
  13. }()
  14. static func getImage(
  15. contact: ContactImageEntry,
  16. state: ContactImageState
  17. ) -> UIImage {
  18. let width = 1024.0
  19. let height = 1024.0
  20. var rect = CGRect(x: 0, y: 0, width: width, height: height)
  21. let textColor: Color = .white.opacity(contact.hasHighContrast ? 1 : 0.8)
  22. let secondaryTextColor: Color = .loopGray.opacity(contact.hasHighContrast ? 1 : 0.8)
  23. let fontWeight = contact.fontWeight
  24. UIGraphicsBeginImageContext(rect.size)
  25. if let context = UIGraphicsGetCurrentContext() {
  26. context.setShouldAntialias(true)
  27. context.setAllowsAntialiasing(true)
  28. }
  29. let ringWidth = Double(contact.ringWidth.rawValue) / 100.0
  30. let ringGap = Double(contact.ringGap.rawValue) / 100.0
  31. let outerGap = 0.03
  32. if contact.ring != .none {
  33. rect = CGRect(
  34. x: rect.minX + width * outerGap,
  35. y: rect.minY + height * outerGap,
  36. width: rect.width - width * outerGap * 2,
  37. height: rect.height - height * outerGap * 2
  38. )
  39. let ringRect = CGRect(
  40. x: rect.minX + width * ringWidth * 0.5,
  41. y: rect.minY + height * ringWidth * 0.5,
  42. width: rect.width - width * ringWidth,
  43. height: rect.height - height * ringWidth
  44. )
  45. drawRing(ring: contact.ring, contact: contact, state: state, rect: ringRect, strokeWidth: width * ringWidth)
  46. rect = CGRect(
  47. x: rect.minX + width * (ringWidth + ringGap),
  48. y: rect.minY + height * (ringWidth + ringGap),
  49. width: rect.width - width * (ringWidth + ringGap) * 2,
  50. height: rect.height - height * (ringWidth + ringGap) * 2
  51. )
  52. }
  53. switch contact.layout {
  54. case .default:
  55. let showTop = contact.top != .none
  56. let showBottom = contact.bottom != .none
  57. let centerX = rect.minX + rect.width / 2
  58. let centerY = rect.minY + rect.height / 2
  59. let radius = min(rect.width, rect.height) / 2
  60. var primaryHeight = radius * 0.8
  61. let topHeight = radius * 0.5
  62. var bottomHeight = radius * 0.5
  63. var primaryY = centerY - primaryHeight / 2
  64. if contact.bottom == .none, contact.top != .none {
  65. primaryY += radius * 0.2
  66. }
  67. if contact.bottom != .none, contact.top == .none {
  68. primaryY -= radius * 0.2
  69. }
  70. let topY = primaryY - topHeight
  71. var bottomY = primaryY + primaryHeight
  72. let primaryWidth = 2 * sqrt(radius * radius - (primaryHeight * 0.5) * (primaryHeight * 0.5))
  73. let topWidth = 2 *
  74. sqrt(radius * radius - (topHeight + primaryHeight * 0.5) * (topHeight + primaryHeight * 0.5))
  75. var bottomWidth = 2 *
  76. sqrt(radius * radius - (bottomHeight + primaryHeight * 0.5) * (bottomHeight + primaryHeight * 0.5))
  77. if contact.bottom != .none, contact.top == .none {
  78. // move things around a little bit to give more space to the bottom area
  79. // TODO: revisit rings for iob, cob and combined iob+cob with more user feedback
  80. if contact.bottom == .trend, contact.ring == .loop {
  81. // if contact.ring == .iob || contact.ring == .cob || contact.ring == .iobcob ||
  82. // (contact.bottom == .trend && contact.ring == .loop)
  83. // {
  84. bottomHeight = bottomHeight + height * ringWidth * 2
  85. bottomWidth = bottomWidth + width * ringWidth * 2
  86. } else if contact.ring == .loop {
  87. primaryHeight = primaryHeight - height * ringWidth
  88. bottomY = primaryY + primaryHeight
  89. bottomHeight = bottomHeight + height * ringWidth * 2
  90. bottomWidth = bottomWidth + width * ringWidth * 2
  91. }
  92. }
  93. let primaryRect = (showTop || showBottom) ? CGRect(
  94. x: centerX - primaryWidth * 0.5,
  95. y: primaryY,
  96. width: primaryWidth,
  97. height: primaryHeight
  98. ) : rect
  99. let topRect = CGRect(
  100. x: centerX - topWidth * 0.5,
  101. y: topY,
  102. width: topWidth,
  103. height: topHeight
  104. )
  105. let bottomRect = CGRect(
  106. x: centerX - bottomWidth * 0.5,
  107. y: bottomY,
  108. width: bottomWidth,
  109. height: bottomHeight
  110. )
  111. let secondaryFontSize = contact.secondaryFontSize
  112. displayPiece(
  113. value: contact.primary,
  114. contact: contact,
  115. state: state,
  116. rect: primaryRect,
  117. fitHeigh: false,
  118. fontSize: contact.fontSize.rawValue,
  119. fontWeight: fontWeight,
  120. fontWidth: contact.fontWidth,
  121. color: textColor
  122. )
  123. if showTop {
  124. displayPiece(
  125. value: contact.top,
  126. contact: contact,
  127. state: state,
  128. rect: topRect,
  129. fitHeigh: true,
  130. fontSize: secondaryFontSize.rawValue,
  131. fontWeight: fontWeight,
  132. fontWidth: contact.fontWidth,
  133. color: secondaryTextColor
  134. )
  135. }
  136. if showBottom {
  137. displayPiece(
  138. value: contact.bottom,
  139. contact: contact,
  140. state: state,
  141. rect: bottomRect,
  142. fitHeigh: true,
  143. fontSize: secondaryFontSize.rawValue,
  144. fontWeight: fontWeight,
  145. fontWidth: contact.fontWidth,
  146. color: secondaryTextColor
  147. )
  148. }
  149. case .split:
  150. let centerX = rect.origin.x + rect.size.width / 2
  151. let centerY = rect.origin.y + rect.size.height / 2
  152. let radius = min(rect.size.width, rect.size.height) / 2
  153. let rectangleHeight = radius * sqrt(2) / 2
  154. let rectangleWidth = sqrt(2) * radius
  155. let topY = centerY - rectangleHeight
  156. let bottomY = centerY
  157. let topRect = CGRect(
  158. x: centerX - rectangleWidth / 2,
  159. y: topY,
  160. width: rectangleWidth,
  161. height: rectangleHeight
  162. )
  163. let bottomRect = CGRect(
  164. x: centerX - rectangleWidth / 2,
  165. y: bottomY,
  166. width: rectangleWidth,
  167. height: rectangleHeight
  168. )
  169. let topFontSize = contact.fontSize
  170. let bottomFontSize = contact.secondaryFontSize
  171. displayPiece(
  172. value: contact.top,
  173. contact: contact,
  174. state: state,
  175. rect: topRect,
  176. fitHeigh: true,
  177. fontSize: topFontSize.rawValue,
  178. fontWeight: fontWeight,
  179. fontWidth: contact.fontWidth,
  180. color: textColor
  181. )
  182. displayPiece(
  183. value: contact.bottom,
  184. contact: contact,
  185. state: state,
  186. rect: bottomRect,
  187. fitHeigh: true,
  188. fontSize: bottomFontSize.rawValue,
  189. fontWeight: fontWeight,
  190. fontWidth: contact.fontWidth,
  191. color: textColor
  192. )
  193. }
  194. let image = UIGraphicsGetImageFromCurrentImageContext()
  195. UIGraphicsEndImageContext()
  196. return image ?? UIImage()
  197. }
  198. private static func displayPiece(
  199. value: ContactImageValue,
  200. contact: ContactImageEntry,
  201. state: ContactImageState,
  202. rect: CGRect,
  203. fitHeigh: Bool,
  204. fontSize: Int,
  205. fontWeight: Font.Weight,
  206. fontWidth: Font.Width,
  207. color: Color
  208. ) {
  209. guard value != .none else { return }
  210. if value == .ring {
  211. drawRing(
  212. ring: .loop,
  213. contact: contact,
  214. state: state,
  215. rect: CGRect(
  216. x: rect.minX + rect.width * 0.10,
  217. y: rect.minY + rect.height * 0.10,
  218. width: rect.width * 0.80,
  219. height: rect.height * 0.80
  220. ),
  221. strokeWidth: 10.0
  222. )
  223. return
  224. }
  225. let text: String? = switch value {
  226. case .glucose: state.glucose
  227. case .eventualBG: state.eventualBG
  228. case .delta: state.delta
  229. case .trend: state.trend
  230. case .lastLoopDate: state.lastLoopDate.map({ formatter.string(from: $0) })
  231. case .cob: state.cobText
  232. case .iob: state.iobText
  233. default: nil
  234. }
  235. let glucoseValue = Decimal(string: state.glucose ?? "100") ?? 100
  236. let dynamicColor: Color = FreeAPS.getDynamicGlucoseColor(
  237. glucoseValue: glucoseValue,
  238. highGlucoseColorValue: state.highGlucoseColorValue,
  239. lowGlucoseColorValue: state.lowGlucoseColorValue,
  240. targetGlucose: state.targetGlucose,
  241. glucoseColorScheme: state.glucoseColorScheme
  242. )
  243. let textColor: Color = switch value {
  244. case .cob:
  245. .loopYellow
  246. case .iob:
  247. .insulin
  248. case .glucose:
  249. dynamicColor
  250. default:
  251. color
  252. }
  253. if let text = text {
  254. drawText(
  255. text: text,
  256. rect: rect,
  257. fitHeigh: fitHeigh,
  258. fontSize: fontSize,
  259. fontWeight: fontWeight,
  260. fontWidth: fontWidth,
  261. color: textColor
  262. )
  263. }
  264. }
  265. private static func drawText(
  266. text: String,
  267. rect: CGRect,
  268. fitHeigh: Bool,
  269. fontSize: Int,
  270. fontWeight: Font.Weight,
  271. fontWidth: Font.Width,
  272. color: Color
  273. ) {
  274. var theFontSize = fontSize
  275. func makeAttributes(_ size: Int) -> [NSAttributedString.Key: Any] {
  276. let font = UIFont.systemFont(ofSize: CGFloat(size), weight: fontWeight.uiFontWeight)
  277. return [
  278. .font: font,
  279. .foregroundColor: UIColor(color),
  280. .kern: fontWidth.value * Double(fontSize) // `kern` is the correct key for tracking
  281. ]
  282. }
  283. var attributes: [NSAttributedString.Key: Any] = makeAttributes(theFontSize)
  284. var stringSize = text.size(withAttributes: attributes)
  285. while stringSize.width > rect.width * 0.90 || fitHeigh && (stringSize.height > rect.height * 0.95), theFontSize > 50 {
  286. theFontSize -= 10
  287. attributes = makeAttributes(theFontSize)
  288. stringSize = text.size(withAttributes: attributes)
  289. }
  290. text.draw(
  291. in: CGRect(
  292. x: rect.minX + (rect.width - stringSize.width) / 2,
  293. y: rect.minY + (rect.height - stringSize.height) / 2,
  294. width: stringSize.width,
  295. height: stringSize.height
  296. ),
  297. withAttributes: attributes
  298. )
  299. }
  300. private static func drawRing(
  301. ring: ContactImageLargeRing,
  302. contact: ContactImageEntry,
  303. state: ContactImageState,
  304. rect: CGRect,
  305. strokeWidth: Double
  306. ) {
  307. guard let context = UIGraphicsGetCurrentContext() else {
  308. return
  309. }
  310. switch ring {
  311. case .loop:
  312. let color = ringColor(contact: contact, state: state)
  313. let strokeWidth = strokeWidth
  314. let center = CGPoint(x: rect.midX, y: rect.midY)
  315. let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2
  316. context.setLineWidth(strokeWidth)
  317. context.setStrokeColor(UIColor(color).cgColor)
  318. context.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: false)
  319. context.strokePath()
  320. // case .iob:
  321. // if let iob = state.iob, state.maxIOB > 0.1 {
  322. // drawProgressBar(
  323. // rect: rect,
  324. // progress: Double(iob) / Double(state.maxIOB),
  325. // colors: [contact.hasHighContrast ? .blue : .blue, contact.hasHighContrast ? .pink : .red],
  326. // strokeWidth: strokeWidth
  327. // )
  328. // }
  329. // case .cob:
  330. // if let cob = state.cob, state.maxCOB > 0.01 {
  331. // drawProgressBar(
  332. // rect: rect,
  333. // progress: Double(cob) / Double(state.maxCOB),
  334. // colors: [.loopYellow, .red],
  335. // strokeWidth: strokeWidth
  336. // )
  337. // }
  338. // case .iobcob:
  339. // if state.maxIOB > 0.01, state.maxCOB > 0.01 {
  340. // drawDoubleProgressBar(
  341. // rect: rect,
  342. // progress1: state.iob.map { Double($0) / Double(state.maxIOB) },
  343. // progress2: state.cob.map { Double($0) / Double(state.maxCOB) },
  344. // colors1: [contact.hasHighContrast ? .blue : .blue, contact.hasHighContrast ? .pink : .red],
  345. // colors2: [.loopYellow, .red],
  346. // strokeWidth: strokeWidth
  347. // )
  348. // }
  349. default:
  350. break
  351. }
  352. }
  353. private static func drawProgressBar(
  354. rect: CGRect,
  355. progress: Double,
  356. colors: [Color],
  357. strokeWidth: Double
  358. ) {
  359. let startAngle: CGFloat = -(.pi + .pi / 4.0)
  360. let endAngle: CGFloat = .pi / 4.0
  361. drawGradientArc(
  362. rect: rect,
  363. progress: progress,
  364. colors: colors,
  365. strokeWidth: strokeWidth,
  366. startAngle: startAngle,
  367. endAngle: endAngle,
  368. gradientDirection: .leftToRight
  369. )
  370. }
  371. private static func drawDoubleProgressBar(
  372. rect: CGRect,
  373. progress1: Double?,
  374. progress2: Double?,
  375. colors1: [Color],
  376. colors2: [Color],
  377. strokeWidth: Double
  378. ) {
  379. if let progress1 = progress1 {
  380. let startAngle1: CGFloat = .pi / 2 + .pi / 5
  381. let endAngle1: CGFloat = 3 * .pi / 2 - .pi / 5
  382. drawGradientArc(
  383. rect: rect,
  384. progress: progress1,
  385. colors: colors1,
  386. strokeWidth: strokeWidth,
  387. startAngle: startAngle1,
  388. endAngle: endAngle1,
  389. gradientDirection: .bottomToTop
  390. )
  391. }
  392. if let progress2 = progress2 {
  393. let startAngle2: CGFloat = .pi / 2 - .pi / 5
  394. let endAngle2: CGFloat = -.pi / 2 + .pi / 5
  395. drawGradientArc(
  396. rect: rect,
  397. progress: progress2,
  398. colors: colors2,
  399. strokeWidth: strokeWidth,
  400. startAngle: startAngle2,
  401. endAngle: endAngle2,
  402. gradientDirection: .bottomToTop
  403. )
  404. }
  405. }
  406. private static func drawGradientArc(
  407. rect: CGRect,
  408. progress: Double,
  409. colors: [Color],
  410. strokeWidth: Double,
  411. startAngle: Double,
  412. endAngle: Double,
  413. gradientDirection: GradientDirection
  414. ) {
  415. guard let context = UIGraphicsGetCurrentContext() else {
  416. return
  417. }
  418. let colors = colors.map { c in UIColor(c).cgColor }
  419. let locations: [CGFloat] = [0.0, 1.0]
  420. guard let gradient = CGGradient(
  421. colorsSpace: CGColorSpaceCreateDeviceRGB(),
  422. colors: colors as CFArray,
  423. locations: locations
  424. ) else {
  425. return
  426. }
  427. context.saveGState()
  428. let center = CGPoint(x: rect.midX, y: rect.midY)
  429. let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2
  430. // angle - The angle to the starting point of the arc, measured in radians from the positive x-axis.
  431. context.setLineWidth(strokeWidth)
  432. context.setLineCap(.round)
  433. let circumference = 2 * .pi * radius
  434. let offsetAngle = (strokeWidth / circumference * 1.1) * 2 * .pi
  435. let (start, middle, end) = if startAngle > endAngle {
  436. (
  437. endAngle,
  438. startAngle - (startAngle - endAngle) * max(min(progress, 1.0), 0.0),
  439. startAngle
  440. )
  441. } else {
  442. (
  443. startAngle,
  444. startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0),
  445. endAngle
  446. )
  447. }
  448. if start < middle - offsetAngle {
  449. let arcPath1 = UIBezierPath()
  450. arcPath1.addArc(
  451. withCenter: center,
  452. radius: radius,
  453. startAngle: start,
  454. endAngle: middle - offsetAngle,
  455. clockwise: true
  456. )
  457. context.addPath(arcPath1.cgPath)
  458. }
  459. if middle + offsetAngle < end {
  460. let arcPath2 = UIBezierPath()
  461. arcPath2.addArc(
  462. withCenter: center,
  463. radius: radius,
  464. startAngle: middle + offsetAngle,
  465. endAngle: end,
  466. clockwise: true
  467. )
  468. context.addPath(arcPath2.cgPath)
  469. }
  470. context.replacePathWithStrokedPath()
  471. context.clip()
  472. switch gradientDirection {
  473. case .bottomToTop:
  474. context.drawLinearGradient(
  475. gradient,
  476. start: CGPoint(x: rect.midX, y: rect.maxY),
  477. end: CGPoint(x: rect.midX, y: rect.minY),
  478. options: []
  479. )
  480. case .leftToRight:
  481. context.drawLinearGradient(
  482. gradient,
  483. start: CGPoint(x: rect.minX, y: rect.midY),
  484. end: CGPoint(x: rect.maxX, y: rect.midY),
  485. options: []
  486. )
  487. }
  488. context.resetClip()
  489. let circleCenter = CGPoint(
  490. x: center.x + radius * cos(middle),
  491. y: center.y + radius * sin(middle)
  492. )
  493. context.setLineWidth(strokeWidth * 0.7)
  494. context.setStrokeColor(UIColor.white.cgColor)
  495. context.addArc(
  496. center: circleCenter,
  497. radius: 0,
  498. startAngle: 0,
  499. endAngle: .pi * 2,
  500. clockwise: true
  501. )
  502. context.strokePath()
  503. context.restoreGState()
  504. }
  505. private static func ringColor(
  506. contact _: ContactImageEntry,
  507. state: ContactImageState
  508. ) -> Color {
  509. guard let lastLoopDate = state.lastLoopDate else {
  510. return .loopGray
  511. }
  512. let delta = Date().timeIntervalSince(lastLoopDate) - Config.lag
  513. if delta <= 5.minutes.timeInterval {
  514. return .loopGreen
  515. } else if delta <= 10.minutes.timeInterval {
  516. return .loopYellow
  517. } else {
  518. return .loopRed
  519. }
  520. }
  521. var uiImage: UIImage {
  522. ContactPicture.getImage(contact: contact, state: state)
  523. }
  524. var body: some View {
  525. Image(uiImage: uiImage)
  526. .frame(width: 256, height: 256)
  527. }
  528. }
  529. extension Font.Weight {
  530. var uiFontWeight: UIFont.Weight {
  531. switch self {
  532. case .ultraLight: return .ultraLight
  533. case .thin: return .thin
  534. case .light: return .light
  535. case .regular: return .regular
  536. case .medium: return .medium
  537. case .semibold: return .semibold
  538. case .bold: return .bold
  539. case .heavy: return .heavy
  540. case .black: return .black
  541. default: return .regular
  542. }
  543. }
  544. }
  545. enum GradientDirection: Int {
  546. case leftToRight
  547. case bottomToTop
  548. }
  549. struct ContactPicturePreview: View {
  550. @Binding var contact: ContactImageEntry
  551. @Binding var state: ContactImageState
  552. var body: some View {
  553. ZStack {
  554. ContactPicture(contact: $contact, state: $state)
  555. Circle()
  556. .stroke(lineWidth: 20)
  557. .foregroundColor(.white)
  558. }
  559. .frame(width: 256, height: 256)
  560. .clipShape(Circle())
  561. .preferredColorScheme(.dark)
  562. }
  563. }
  564. struct ContactPicture_Previews: PreviewProvider {
  565. struct Preview: View {
  566. @State var rangeIndicator: Bool = true
  567. @State var hasHighContrast: Bool = true
  568. @State var fontSize: ContactImageEntry.FontSize = .small
  569. @State var fontWeight: UIFont.Weight = .bold
  570. @State var fontName: String? = "AmericanTypewriter"
  571. var body: some View {
  572. ContactPicturePreview(
  573. contact: .constant(
  574. ContactImageEntry(
  575. primary: .glucose,
  576. top: .delta,
  577. bottom: .trend,
  578. fontSize: fontSize,
  579. fontWeight: .medium
  580. )
  581. ),
  582. state: .constant(ContactImageState(
  583. glucose: "6.8",
  584. trend: "↗︎",
  585. delta: "+0.2",
  586. cob: 25,
  587. cobText: "25"
  588. ))
  589. ).previewDisplayName("bg + trend + delta")
  590. // ContactPicturePreview(
  591. // contact: .constant(
  592. // ContactImageEntry(
  593. // ring: .iob,
  594. // primary: .glucose,
  595. // bottom: .trend,
  596. // fontSize: fontSize,
  597. // fontWeight: .medium
  598. // )
  599. // ),
  600. // state: .constant(ContactImageState(
  601. // glucose: "6.8",
  602. // trend: "↗︎",
  603. // iob: 6.1,
  604. // iobText: "6.1",
  605. // maxIOB: 8.0
  606. // ))
  607. // ).previewDisplayName("bg + trend + iob ring")
  608. ContactPicturePreview(
  609. contact: .constant(
  610. ContactImageEntry(
  611. primary: .glucose,
  612. top: .ring,
  613. bottom: .trend,
  614. fontSize: fontSize,
  615. fontWeight: .medium
  616. )
  617. ),
  618. state: .constant(ContactImageState(
  619. glucose: "6.8",
  620. trend: "↗︎",
  621. lastLoopDate: .now
  622. ))
  623. ).previewDisplayName("bg + trend + ring")
  624. ContactPicturePreview(
  625. contact: .constant(
  626. ContactImageEntry(
  627. ring: .loop,
  628. primary: .glucose,
  629. top: .none,
  630. bottom: .trend,
  631. fontSize: fontSize,
  632. fontWeight: .medium
  633. )
  634. ),
  635. state: .constant(ContactImageState(
  636. glucose: "8.8",
  637. trend: "→",
  638. lastLoopDate: .now
  639. ))
  640. ).previewDisplayName("bg + trend + ring")
  641. ContactPicturePreview(
  642. contact: .constant(
  643. ContactImageEntry(
  644. ring: .loop,
  645. primary: .glucose,
  646. top: .none,
  647. bottom: .eventualBG,
  648. fontSize: fontSize,
  649. fontWeight: .medium
  650. )
  651. ),
  652. state: .constant(ContactImageState(
  653. glucose: "6.8",
  654. lastLoopDate: .now - 7.minutes,
  655. eventualBG: "6.2"
  656. ))
  657. ).previewDisplayName("bg + eventual + ring")
  658. ContactPicturePreview(
  659. contact: .constant(
  660. ContactImageEntry(
  661. ring: .loop,
  662. primary: .lastLoopDate,
  663. top: .none,
  664. bottom: .none,
  665. fontSize: fontSize,
  666. fontWeight: .medium
  667. )
  668. ),
  669. state: .constant(ContactImageState(
  670. glucose: "6.8",
  671. trend: "↗︎",
  672. lastLoopDate: .now - 2.minutes
  673. ))
  674. ).previewDisplayName("lastLoopDate + ring")
  675. ContactPicturePreview(
  676. contact: .constant(
  677. ContactImageEntry(
  678. ring: .loop,
  679. primary: .glucose,
  680. top: .none,
  681. bottom: .none,
  682. fontSize: fontSize,
  683. fontWeight: .medium
  684. )
  685. ),
  686. state: .constant(ContactImageState(
  687. glucose: "6.8",
  688. lastLoopDate: .now,
  689. iob: 6.1,
  690. iobText: "6.1",
  691. maxIOB: 8.0
  692. ))
  693. ).previewDisplayName("bg + ring + ring2")
  694. ContactPicturePreview(
  695. contact: .constant(
  696. ContactImageEntry(
  697. layout: .split,
  698. top: .iob,
  699. bottom: .cob,
  700. fontSize: fontSize,
  701. fontWeight: .medium
  702. )
  703. ),
  704. state: .constant(ContactImageState(
  705. iob: 1.5,
  706. iobText: "1.5",
  707. cob: 25,
  708. cobText: "25"
  709. ))
  710. ).previewDisplayName("iob + cob")
  711. // ContactPicturePreview(
  712. // contact: .constant(
  713. // ContactImageEntry(
  714. // layout: .default,
  715. // ring: .iobcob,
  716. // primary: .none,
  717. // ringWidth: .regular,
  718. // ringGap: .regular,
  719. // fontSize: fontSize,
  720. // fontWeight: .medium
  721. // )
  722. // ),
  723. // state: .constant(ContactImageState(
  724. // iob: 1,
  725. // iobText: "5.5",
  726. // cob: 25,
  727. // cobText: "25",
  728. // maxIOB: 10,
  729. // maxCOB: 120
  730. // ))
  731. // ).previewDisplayName("iobcob ring")
  732. //
  733. // ContactPicturePreview(
  734. // contact: .constant(
  735. // ContactImageEntry(
  736. // layout: .default,
  737. // ring: .iobcob,
  738. // primary: .none,
  739. // fontSize: fontSize,
  740. // fontWeight: .medium
  741. // )
  742. // ),
  743. // state: .constant(ContactImageState(
  744. // iob: -0.2,
  745. // iobText: "0.0",
  746. // cob: 0,
  747. // cobText: "0",
  748. // maxIOB: 10,
  749. // maxCOB: 120
  750. // ))
  751. // ).previewDisplayName("iobcob ring (0/0)")
  752. //
  753. // ContactPicturePreview(
  754. // contact: .constant(
  755. // ContactImageEntry(
  756. // layout: .default,
  757. // ring: .iobcob,
  758. // primary: .none,
  759. // fontSize: fontSize,
  760. // fontWeight: .medium
  761. // )
  762. // ),
  763. // state: .constant(ContactImageState(
  764. // iob: 10,
  765. // iobText: "0.0",
  766. // cob: 120,
  767. // cobText: "0",
  768. // maxIOB: 10,
  769. // maxCOB: 120
  770. // ))
  771. // ).previewDisplayName("iobcob ring (max/max)")
  772. //
  773. // ContactPicturePreview(
  774. // contact: .constant(
  775. // ContactImageEntry(
  776. // layout: .default,
  777. // ring: .iobcob,
  778. // primary: .glucose,
  779. // bottom: .trend,
  780. // fontSize: fontSize,
  781. // fontWeight: .medium
  782. // )
  783. // ),
  784. // state: .constant(ContactImageState(
  785. // glucose: "6.8",
  786. // trend: "↗︎",
  787. // iob: 5.5,
  788. // iobText: "5.5",
  789. // cob: 25,
  790. // cobText: "25",
  791. // maxIOB: 10,
  792. // maxCOB: 120
  793. // ))
  794. // ).previewDisplayName("bg + trend + iobcob ring")
  795. }
  796. }
  797. static var previews: some View {
  798. Preview()
  799. }
  800. }