ViewModifiers.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import Combine
  2. import SwiftUI
  3. struct RoundedBackground: ViewModifier {
  4. private let color: Color
  5. init(color: Color = Color("CapsuleColor")) {
  6. self.color = color
  7. }
  8. func body(content: Content) -> some View {
  9. content
  10. .padding()
  11. .background(
  12. RoundedRectangle(cornerRadius: 8, style: .continuous)
  13. .fill()
  14. .foregroundColor(color)
  15. )
  16. }
  17. }
  18. struct CapsulaBackground: ViewModifier {
  19. private let color: Color
  20. init(color: Color = Color("CapsuleColor")) {
  21. self.color = color
  22. }
  23. func body(content: Content) -> some View {
  24. content
  25. .padding()
  26. .background(
  27. Capsule()
  28. .fill()
  29. .foregroundColor(color)
  30. )
  31. }
  32. }
  33. private let navigationCache = LRUCache<Screen.ID, AnyView>(capacity: 10)
  34. struct NavigationLazyView: View {
  35. let build: () -> AnyView
  36. let screen: Screen
  37. init(_ build: @autoclosure @escaping () -> AnyView, screen: Screen) {
  38. self.build = build
  39. self.screen = screen
  40. }
  41. var body: AnyView {
  42. if navigationCache[screen.id] == nil {
  43. navigationCache[screen.id] = build()
  44. }
  45. return navigationCache[screen.id]!
  46. .onDisappear {
  47. navigationCache[screen.id] = nil
  48. }.asAny()
  49. }
  50. }
  51. struct Link: ViewModifier {
  52. let screen: Screen
  53. init(screen: Screen) {
  54. self.screen = screen
  55. }
  56. func body(content: Content) -> some View {
  57. NavigationLink(value: screen, label: { content })
  58. }
  59. }
  60. struct ScreenNavigation<T>: ViewModifier where T: View {
  61. private let destination: (Screen) -> T
  62. init(destination: @escaping (Screen) -> T) {
  63. self.destination = destination
  64. }
  65. func body(content: Content) -> some View {
  66. content.navigationDestination(
  67. for: Screen.self,
  68. destination: { screen in NavigationLazyView(destination(screen).asAny(), screen: screen) }
  69. )
  70. }
  71. }
  72. struct AdaptsToSoftwareKeyboard: ViewModifier {
  73. @State var currentHeight: CGFloat = 0
  74. func body(content: Content) -> some View {
  75. content
  76. .padding(.bottom, currentHeight).animation(.easeOut(duration: 0.25))
  77. .edgesIgnoringSafeArea(currentHeight == 0 ? Edge.Set() : .bottom)
  78. .onAppear(perform: subscribeToKeyboardChanges)
  79. }
  80. private let keyboardHeightOnOpening = Foundation.NotificationCenter.default
  81. .publisher(for: UIResponder.keyboardWillShowNotification)
  82. .map { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
  83. .map(\.height)
  84. private let keyboardHeightOnHiding = Foundation.NotificationCenter.default
  85. .publisher(for: UIResponder.keyboardWillHideNotification)
  86. .map { _ in CGFloat(0) }
  87. private func subscribeToKeyboardChanges() {
  88. _ = Publishers.Merge(keyboardHeightOnOpening, keyboardHeightOnHiding)
  89. .subscribe(on: DispatchQueue.main)
  90. .sink { height in
  91. if self.currentHeight == 0 || height == 0 {
  92. self.currentHeight = height
  93. }
  94. }
  95. }
  96. }
  97. struct ClearButton: ViewModifier {
  98. @Binding var text: String
  99. func body(content: Content) -> some View {
  100. HStack {
  101. content
  102. if !text.isEmpty {
  103. Button { self.text = "" }
  104. label: {
  105. Image(systemName: "delete.left")
  106. .foregroundColor(.gray)
  107. }
  108. }
  109. }
  110. }
  111. }
  112. extension View {
  113. func roundedBackground() -> some View {
  114. modifier(RoundedBackground())
  115. }
  116. func buttonBackground() -> some View {
  117. modifier(RoundedBackground(color: .accentColor))
  118. }
  119. func navigationLink<V: BaseView>(to screen: Screen, from _: V) -> some View {
  120. modifier(Link(screen: screen))
  121. }
  122. func screenNavigation<V: BaseView>(_ view: V) -> some View {
  123. modifier(ScreenNavigation { screen in
  124. view.state.view(for: screen)
  125. })
  126. }
  127. func adaptsToSoftwareKeyboard() -> some View {
  128. modifier(AdaptsToSoftwareKeyboard())
  129. }
  130. func modal<V: BaseView>(for screen: Screen?, from view: V) -> some View {
  131. onTapGesture {
  132. view.state.showModal(for: screen)
  133. }
  134. }
  135. func asAny() -> AnyView { .init(self) }
  136. var backport: Backport<Self> { Backport(content: self) }
  137. }
  138. struct Backport<Content: View> {
  139. let content: Content
  140. }
  141. extension Backport {
  142. @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
  143. if (state as? Treatments.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
  144. (state as? Home.StateModel)?.forecastDisplayType == ForecastDisplayType.lines
  145. {
  146. let modifiedContent = content
  147. .chartForegroundStyleScale([
  148. "iob": .blue,
  149. "uam": Color.uam,
  150. "zt": Color.zt,
  151. "cob": .orange
  152. ])
  153. if state is Home.StateModel {
  154. modifiedContent
  155. .chartLegend(.hidden)
  156. } else {
  157. modifiedContent
  158. }
  159. } else {
  160. content
  161. }
  162. }
  163. }