ConfigurationPage.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. //
  2. // ConfigurationPage.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/10/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. public enum ConfigurationPageActionButtonState {
  10. case enabled
  11. case loading
  12. case disabled
  13. }
  14. public struct ConfigurationPage<ActionAreaContent: View>: View {
  15. public typealias ActionButtonState = ConfigurationPageActionButtonState
  16. var title: Text
  17. var actionButtonTitle: Text
  18. var actionButtonState: ActionButtonState
  19. var cardListStyle: CardListStyle
  20. var actionAreaContent: ActionAreaContent
  21. var action: () -> Void
  22. public var body: some View {
  23. VStack(spacing: 0) {
  24. CardList(title: title, style: cardListStyle)
  25. VStack(spacing: 0) {
  26. actionAreaContent
  27. .padding([.top, .horizontal])
  28. .transition(AnyTransition.opacity.combined(with: .move(edge: .bottom)))
  29. Button(
  30. action: action,
  31. label: {
  32. HStack(spacing: 12) {
  33. ActivityIndicator(isAnimating: .constant(false), style: .medium)
  34. .opacity(0) // For layout only, to ensure the button text is centered
  35. actionButtonTitle
  36. .animation(nil)
  37. ActivityIndicator(isAnimating: .constant(true), style: .medium, color: .white)
  38. .opacity(actionButtonState == .loading ? 1 : 0)
  39. }
  40. }
  41. )
  42. .buttonStyle(ActionButtonStyle(.primary))
  43. .disabled(actionButtonState != .enabled)
  44. .padding()
  45. }
  46. .padding(.bottom) // FIXME: unnecessary on iPhone 8 size devices
  47. .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5))
  48. }
  49. .edgesIgnoringSafeArea(.bottom)
  50. }
  51. }
  52. extension ConfigurationPage {
  53. public init(
  54. title: Text,
  55. actionButtonTitle: Text,
  56. actionButtonState: ActionButtonState = .enabled,
  57. @CardStackBuilder cards: () -> CardStack,
  58. @ViewBuilder actionAreaContent: () -> ActionAreaContent,
  59. action: @escaping () -> Void
  60. ) {
  61. self.title = title
  62. self.actionButtonTitle = actionButtonTitle
  63. self.actionButtonState = actionButtonState
  64. self.cardListStyle = .simple(cards())
  65. self.actionAreaContent = actionAreaContent()
  66. self.action = action
  67. }
  68. /// Convenience initializer for a page whose action is 'Save'
  69. public init(
  70. title: Text,
  71. saveButtonState: ActionButtonState = .enabled,
  72. @CardStackBuilder cards: () -> CardStack,
  73. @ViewBuilder actionAreaContent: () -> ActionAreaContent,
  74. onSave save: @escaping () -> Void
  75. ) {
  76. self.init(
  77. title: title,
  78. actionButtonTitle: Text(LocalizedString("Save", comment: "The button text for saving on a configuration page")),
  79. actionButtonState: saveButtonState,
  80. cards: cards,
  81. actionAreaContent: actionAreaContent,
  82. action: save
  83. )
  84. }
  85. /// Convenience initializer for a sectioned page whose action is 'Save'
  86. public init(
  87. title: Text,
  88. saveButtonState: ActionButtonState = .enabled,
  89. sections: [CardListSection],
  90. @ViewBuilder actionAreaContent: () -> ActionAreaContent,
  91. onSave save: @escaping () -> Void
  92. ) {
  93. self.init(
  94. title: title,
  95. actionButtonTitle: Text(LocalizedString("Save", comment: "The button text for saving on a configuration page")),
  96. actionButtonState: saveButtonState,
  97. cardListStyle: .sectioned(sections),
  98. actionAreaContent: actionAreaContent(),
  99. action: save
  100. )
  101. }
  102. }
  103. struct ConfigurationPage_Previews: PreviewProvider {
  104. static var previews: some View {
  105. ConfigurationPage(
  106. title: Text("Example"),
  107. cards: {
  108. Text("A simple card")
  109. Text("A card whose text will wrap onto multiple lines if I continue to type for long enough—this length should do")
  110. Card {
  111. Text("Top component")
  112. Text("Bottom component")
  113. }
  114. Card(of: 1...3, id: \.self) { value in
  115. Text("Dynamic component #\(value)")
  116. }
  117. },
  118. actionAreaContent: {
  119. Text("Above the save button")
  120. },
  121. onSave: {}
  122. )
  123. }
  124. }