TrendShape.swift 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import SwiftUI
  2. struct Triangle: Shape {
  3. /// Flag to be able to adjust size based on Apple Watch size
  4. let deviceType: WatchSize
  5. private var triangleTipFactor: CGFloat {
  6. switch deviceType {
  7. case .watch40mm,
  8. .watch41mm,
  9. .watch42mm:
  10. return 7.5
  11. case .unknown,
  12. .watch44mm,
  13. .watch45mm:
  14. return 9
  15. case .watch49mm:
  16. return 9
  17. }
  18. }
  19. private var triangleBezierFactor: CGFloat {
  20. switch deviceType {
  21. case .watch40mm,
  22. .watch41mm,
  23. .watch42mm:
  24. return 5
  25. case .unknown,
  26. .watch44mm,
  27. .watch45mm:
  28. return 7
  29. case .watch49mm:
  30. return 7
  31. }
  32. }
  33. /// Creates a triangle shape pointing to the right
  34. func path(in rect: CGRect) -> Path {
  35. var path = Path()
  36. // Draw the triangle pointing to the right
  37. path.move(to: CGPoint(x: rect.maxX - triangleTipFactor, y: rect.midY))
  38. path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
  39. path.addQuadCurve(
  40. to: CGPoint(x: rect.minX, y: rect.maxY),
  41. control: CGPoint(x: rect.midX - triangleBezierFactor, y: rect.midY)
  42. )
  43. path.closeSubpath()
  44. return path
  45. }
  46. }
  47. /// A view that displays a circular trend indicator with a directional triangle
  48. struct TrendShape: View {
  49. let isWatchStateDated: Bool
  50. /// Rotation angle in degrees for the trend direction
  51. let rotationDegrees: Double
  52. /// Flag to be able to adjust size based on Apple Watch size
  53. let deviceType: WatchSize
  54. // Angular gradient for the outer circle, transitioning through various blues and purples
  55. private let angularGradient = AngularGradient(
  56. colors: [
  57. Color(red: 0.7215686275, green: 0.3411764706, blue: 1), // #B857FF
  58. Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569), // #9F6CFA
  59. Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765), // #7C8BF3
  60. Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961), // #57AAEC
  61. Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902), // #43BBE9
  62. Color(red: 0.7215686275, green: 0.3411764706, blue: 1) // #B857FF (repeated for seamless transition)
  63. ],
  64. center: .center,
  65. startAngle: .degrees(270),
  66. endAngle: .degrees(-90)
  67. )
  68. private let staleWatchStateGradient = AngularGradient(
  69. colors: [
  70. Color.secondary,
  71. Color.secondary.opacity(0.8),
  72. Color.secondary.opacity(0.6),
  73. Color.secondary.opacity(0.4),
  74. Color.secondary
  75. ],
  76. center: .center
  77. )
  78. // Color for the direction indicator triangle
  79. private let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902) // #43BBE9
  80. private var strokeWidth: CGFloat {
  81. switch deviceType {
  82. case .watch40mm:
  83. return 3
  84. case .watch41mm,
  85. .watch42mm:
  86. return 4
  87. case .unknown,
  88. .watch44mm,
  89. .watch45mm:
  90. return 4
  91. case .watch49mm:
  92. return 5
  93. }
  94. }
  95. private var circleSize: CGFloat {
  96. switch deviceType {
  97. case .watch40mm:
  98. return 72
  99. case .watch41mm,
  100. .watch42mm:
  101. return 74
  102. case .watch44mm:
  103. return 82
  104. case .unknown,
  105. .watch45mm:
  106. return 90
  107. case .watch49mm:
  108. return 92
  109. }
  110. }
  111. private var triangleSize: CGFloat {
  112. switch deviceType {
  113. case .watch40mm,
  114. .watch41mm,
  115. .watch42mm:
  116. return 16
  117. case .watch44mm:
  118. return 18
  119. case .unknown,
  120. .watch45mm:
  121. return 20
  122. case .watch49mm:
  123. return 20
  124. }
  125. }
  126. private var triangleOffset: CGFloat {
  127. switch deviceType {
  128. case .watch40mm:
  129. return 46
  130. case .watch41mm,
  131. .watch42mm:
  132. return 47.5
  133. case .watch44mm:
  134. return 53.5
  135. case .unknown,
  136. .watch45mm:
  137. return 58
  138. case .watch49mm:
  139. return 59
  140. }
  141. }
  142. var body: some View {
  143. ZStack {
  144. // Outer circle with gradient
  145. Circle()
  146. .stroke(isWatchStateDated ? staleWatchStateGradient : angularGradient, lineWidth: strokeWidth)
  147. .frame(width: circleSize, height: circleSize)
  148. .background(Circle().fill(Color.black))
  149. // Triangle with the color of the last gradient color
  150. Triangle(deviceType: deviceType)
  151. .fill(triangleColor)
  152. .frame(width: triangleSize, height: triangleSize)
  153. .offset(x: triangleOffset)
  154. .opacity(isWatchStateDated ? 0 : 1)
  155. }
  156. .rotationEffect(.degrees(rotationDegrees))
  157. .shadow(color: Color.black.opacity(0.33), radius: 3)
  158. }
  159. }