Card.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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. public var body: some View {
  43. VStack {
  44. ForEach(parts.indices, id: \.self) { index in
  45. Group {
  46. if self.parts[index] != nil {
  47. VStack {
  48. self.parts[index]!
  49. .padding(.top, 4)
  50. if index != self.parts.indices.last! {
  51. CardSectionDivider()
  52. }
  53. }
  54. .transition(AnyTransition.move(edge: .top).combined(with: .opacity))
  55. }
  56. }
  57. }
  58. }
  59. .frame(maxWidth: .infinity)
  60. .padding()
  61. .background(CardBackground())
  62. .padding(.horizontal)
  63. }
  64. }
  65. extension Card {
  66. public enum Component {
  67. case `static`(AnyView)
  68. case dynamic([(view: AnyView, id: AnyHashable)])
  69. }
  70. public init(@CardBuilder card: () -> Card) {
  71. self = card()
  72. }
  73. public init<Data: RandomAccessCollection, ID: Hashable, Content: View>(
  74. of data: Data,
  75. id: KeyPath<Data.Element, ID>,
  76. rowContent: (Data.Element) -> Content
  77. ) {
  78. self.init(components: [.dynamic(Splat(data, id: id, rowContent: rowContent).identifiedViews)])
  79. }
  80. public init<Data: RandomAccessCollection, Content: View>(
  81. of data: Data,
  82. @ViewBuilder rowContent: (Data.Element) -> Content
  83. ) where Data.Element: Identifiable {
  84. self.init(of: data, id: \.id, rowContent: rowContent)
  85. }
  86. init(reducing cards: [Card]) {
  87. self.parts = cards.flatMap { $0.parts }
  88. }
  89. /// `nil` values denote placeholder positions where a view may become visible upon state change.
  90. init(components: [Component?]) {
  91. self.parts = components.map { component in
  92. switch component {
  93. case .static(let view):
  94. return view
  95. case .dynamic(let identifiedViews):
  96. return AnyView(
  97. ForEach(identifiedViews, id: \.id) { view, id in
  98. VStack {
  99. view
  100. if id != identifiedViews.last?.id {
  101. CardSectionDivider()
  102. }
  103. }
  104. }
  105. )
  106. case nil:
  107. return nil
  108. }
  109. }
  110. }
  111. }
  112. private struct CardBackground: View {
  113. var body: some View {
  114. RoundedRectangle(cornerRadius: 10)
  115. .foregroundColor(Color(.secondarySystemGroupedBackground))
  116. }
  117. }
  118. private struct CardSectionDivider: View {
  119. var body: some View {
  120. Divider()
  121. .padding(.trailing, -16)
  122. }
  123. }