1
0

DatePicker.swift 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import Foundation
  2. public struct DatePickerComponents: OptionSet, Sendable {
  3. public let rawValue: UInt
  4. public init(rawValue: UInt) {
  5. self.rawValue = rawValue
  6. }
  7. public init() {
  8. self.rawValue = 0
  9. }
  10. // These magic numbers are the same as SwiftUI. It's actually a bitfield:
  11. //
  12. // smhdMy--
  13. // date 00011100
  14. // hourAndMinute 01100000
  15. // hourMinuteAndSecond 11100000
  16. //
  17. // Like SwiftUI, not all combinations are valid (SwiftUI fatalErrors if you try to get creative
  18. // with your choice of flags), and hourMinuteAndSecond intentionally includes hourAndMinute.
  19. public static let date = DatePickerComponents(rawValue: 0x1C)
  20. public static let hourAndMinute = DatePickerComponents(rawValue: 0x60)
  21. @available(iOS, unavailable)
  22. @available(visionOS, unavailable)
  23. @available(macCatalyst, unavailable)
  24. public static let hourMinuteAndSecond = DatePickerComponents(rawValue: 0xE0)
  25. }
  26. public enum DatePickerStyle: Sendable, Hashable {
  27. /// A date input that adapts to the current platform and context.
  28. case automatic
  29. /// A date input that shows a calendar grid.
  30. @available(iOS 14, macCatalyst 14, *)
  31. case graphical
  32. /// A smaller date input. This may be a text field, or a button that opens a calendar pop-up.
  33. @available(iOS 13.4, macCatalyst 13.4, *)
  34. case compact
  35. /// A set of scrollable inputs that can be used to select a date.
  36. @available(iOS 13.4, macCatalyst 13.4, *)
  37. @available(macOS, unavailable)
  38. case wheel
  39. }
  40. @available(tvOS, unavailable)
  41. public struct DatePicker<Label: View> {
  42. private var label: Label
  43. private var selection: Binding<Date>
  44. private var range: ClosedRange<Date>
  45. private var components: DatePickerComponents
  46. private var style: DatePickerStyle = .automatic
  47. /// Displays a date input.
  48. /// - Parameters:
  49. /// - selection: The currently-selected date.
  50. /// - range: The range of dates to display. The backend takes this as a hint but it is not
  51. /// necessarily enforced. As such this parameter should be treated as an aid to validation
  52. /// rather than a replacement for it.
  53. /// - displayedComponents: Which parts of the date/time to display in the input.
  54. /// - label: The view to be shown next to the date input.
  55. public nonisolated init(
  56. selection: Binding<Date>,
  57. in range: ClosedRange<Date> = Date.distantPast...Date.distantFuture,
  58. displayedComponents: DatePickerComponents = [.hourAndMinute, .date],
  59. @ViewBuilder label: () -> Label
  60. ) {
  61. self.label = label()
  62. self.selection = selection
  63. self.range = range
  64. self.components = displayedComponents
  65. }
  66. /// Displays a date input.
  67. /// - Parameters:
  68. /// - label: The text to be shown next to the date input.
  69. /// - selection: The currently-selected date.
  70. /// - range: The range of dates to display. The backend takes this as a hint but it is not
  71. /// necessarily enforced. As such this parameter should be treated as an aid to validation
  72. /// rather than a replacement for it.
  73. /// - displayedComponents: Which parts of the date/time to display in the input.
  74. public nonisolated init(
  75. _ label: String,
  76. selection: Binding<Date>,
  77. in range: ClosedRange<Date> = Date.distantPast...Date.distantFuture,
  78. displayedComponents: DatePickerComponents = [.hourAndMinute, .date]
  79. ) where Label == Text {
  80. self.label = Text(label)
  81. self.selection = selection
  82. self.range = range
  83. self.components = displayedComponents
  84. }
  85. public typealias Components = DatePickerComponents
  86. }
  87. @available(tvOS, unavailable)
  88. extension DatePicker: View {
  89. public var body: some View {
  90. HStack {
  91. label
  92. DatePickerImplementation(selection: selection, range: range, components: components)
  93. }
  94. }
  95. }
  96. @available(tvOS, unavailable)
  97. internal struct DatePickerImplementation: ElementaryView {
  98. @Binding private var selection: Date
  99. private var range: ClosedRange<Date>
  100. private var components: DatePickerComponents
  101. init(selection: Binding<Date>, range: ClosedRange<Date>, components: DatePickerComponents) {
  102. self._selection = selection
  103. self.range = range
  104. self.components = components
  105. }
  106. let body = EmptyView()
  107. func asWidget<Backend: BaseAppBackend>(backend: Backend) -> Backend.Widget {
  108. func castBackend_inner<NewBackend: BaseAppBackend & BackendFeatures.DatePickers>(_ backend: NewBackend) -> NewBackend.Widget {
  109. return backend.createDatePicker()
  110. }
  111. guard let castedBackend = backend as? any (BaseAppBackend & BackendFeatures.DatePickers) else {
  112. fatalError("Backend does not implement BackendFeatures.DatePickers")
  113. }
  114. return castBackend_inner(castedBackend) as! Backend.Widget
  115. }
  116. func computeLayout<Backend: BaseAppBackend>(
  117. _ widget: Backend.Widget,
  118. proposedSize: ProposedViewSize,
  119. environment: EnvironmentValues,
  120. backend: Backend
  121. ) -> ViewLayoutResult {
  122. func castBackend_inner<NewBackend: BaseAppBackend & BackendFeatures.DatePickers>(_ backend: NewBackend) -> ViewLayoutResult {
  123. let widget = widget as! NewBackend.Widget
  124. backend.updateDatePicker(
  125. widget,
  126. environment: environment,
  127. date: selection,
  128. range: range,
  129. components: components,
  130. onChange: { selection = $0 }
  131. )
  132. // I reject your proposedSize and substitute my own
  133. let naturalSize = backend.naturalSize(of: widget)
  134. return ViewLayoutResult.leafView(size: ViewSize(naturalSize))
  135. }
  136. guard let castedBackend = backend as? any (BaseAppBackend & BackendFeatures.DatePickers) else {
  137. fatalError("Backend does not implement BackendFeatures.DatePickers")
  138. }
  139. return castBackend_inner(castedBackend)
  140. }
  141. func commit<Backend: BaseAppBackend>(
  142. _ widget: Backend.Widget,
  143. layout: ViewLayoutResult,
  144. environment: EnvironmentValues,
  145. backend: Backend
  146. ) {
  147. backend.setSize(of: widget, to: layout.size.vector)
  148. }
  149. }