Card.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. //
  2. // Card.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/14/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. /// A platter displaying a number of components over a rounded background tile.
  10. ///
  11. /// In a `CardStackBuilder`, a single-component `Card` is implicitly created from any expression conforming to `View`.
  12. /// A multi-component `Card` can be constructed using one of `Card`'s initializers.
  13. ///
  14. /// A multi-component card may consist of purely static components:
  15. /// ```
  16. /// Card {
  17. /// Text("Top")
  18. /// Text("Middle")
  19. /// Text("Bottom")
  20. /// }
  21. /// ```
  22. ///
  23. /// Cards of a dynamic number of components can be constructed from identifiable data:
  24. /// ```
  25. /// Card(of: 1...5, id: \.self) { value in
  26. /// Text("\(value)")
  27. /// }
  28. /// ```
  29. ///
  30. /// Finally, dynamic components can be unrolled to intermix with static components via `Splat`:
  31. /// ```
  32. /// Card {
  33. /// Text("Above dynamic data")
  34. /// Splat(1...5, id: \.self) { value in
  35. /// Text("Dynamic data \(value)")
  36. /// }
  37. /// Text("Below dynamic data")
  38. /// }
  39. /// ```
  40. public struct Card: View {
  41. var parts: [AnyView?]
  42. var backgroundColor: Color = Color(.secondarySystemGroupedBackground)
  43. public var body: some View {
  44. VStack {
  45. ForEach(parts.indices, id: \.self) { index in
  46. Group {
  47. if self.parts[index] != nil {
  48. VStack {
  49. self.parts[index]!
  50. .padding(.top, 4)
  51. if index != self.parts.indices.last! {
  52. CardSectionDivider()
  53. }
  54. }
  55. .transition(AnyTransition.move(edge: .top).combined(with: .opacity))
  56. }
  57. }
  58. }
  59. }
  60. .frame(maxWidth: .infinity)
  61. .padding()
  62. .background(CardBackground(color: backgroundColor))
  63. .padding(.horizontal)
  64. }
  65. }
  66. extension Card {
  67. init(_ other: Self, backgroundColor: Color? = nil) {
  68. self.parts = other.parts
  69. self.backgroundColor = backgroundColor ?? other.backgroundColor
  70. }
  71. func backgroundColor(_ color: Color?) -> Self { Self(self, backgroundColor: color) }
  72. }
  73. extension Card {
  74. public enum Component {
  75. case `static`(AnyView)
  76. case dynamic([(view: AnyView, id: AnyHashable)])
  77. }
  78. public init(@CardBuilder card: () -> Card) {
  79. self = card()
  80. }
  81. public init<Data: RandomAccessCollection, ID: Hashable, Content: View>(
  82. of data: Data,
  83. id: KeyPath<Data.Element, ID>,
  84. rowContent: (Data.Element) -> Content
  85. ) {
  86. self.init(components: [.dynamic(Splat(data, id: id, rowContent: rowContent).identifiedViews)])
  87. }
  88. public init<Data: RandomAccessCollection, Content: View>(
  89. of data: Data,
  90. @ViewBuilder rowContent: (Data.Element) -> Content
  91. ) where Data.Element: Identifiable {
  92. self.init(of: data, id: \.id, rowContent: rowContent)
  93. }
  94. init(reducing cards: [Card]) {
  95. self.parts = cards.flatMap { $0.parts }
  96. }
  97. /// `nil` values denote placeholder positions where a view may become visible upon state change.
  98. init(components: [Component?]) {
  99. self.parts = components.map { component in
  100. switch component {
  101. case .static(let view):
  102. return view
  103. case .dynamic(let identifiedViews):
  104. return AnyView(
  105. ForEach(identifiedViews, id: \.id) { view, id in
  106. VStack {
  107. view
  108. if id != identifiedViews.last?.id {
  109. CardSectionDivider()
  110. }
  111. }
  112. }
  113. )
  114. case nil:
  115. return nil
  116. }
  117. }
  118. }
  119. }
  120. private struct CardBackground: View {
  121. var color: Color = Color(.secondarySystemGroupedBackground)
  122. var body: some View {
  123. RoundedRectangle(cornerRadius: 10)
  124. .foregroundColor(color)
  125. }
  126. }
  127. private struct CardSectionDivider: View {
  128. var body: some View {
  129. Divider()
  130. .padding(.trailing, -16)
  131. }
  132. }