Deletable.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. //
  2. // Deletable.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/30/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. enum TableDeletionState: Equatable {
  10. case disabled
  11. case enabled
  12. case awaitingConfirmation(toDeleteItemAt: Int)
  13. var isAwaitingConfirmation: Bool {
  14. if case .awaitingConfirmation = self {
  15. return true
  16. } else {
  17. return false
  18. }
  19. }
  20. var indexAwaitingDeletionConfirmation: Int? {
  21. if case .awaitingConfirmation(toDeleteItemAt: let index) = self {
  22. return index
  23. } else {
  24. return nil
  25. }
  26. }
  27. }
  28. /// Mimics the behavior of UITableViewCell deletion.
  29. ///
  30. /// As of Xcode 11.4, SwiftUI's List does not play nicely with resizing cells.
  31. /// CardList solves this issue while retaining the appearance of an inset grouped table.
  32. /// However, by avoiding List, we also lose built-in deletion functionality, requiring this implementation.
  33. struct Deletable<Content: View>: View {
  34. @Binding var tableDeletionState: TableDeletionState
  35. var index: Int
  36. var isDeletable: Bool
  37. var delete: () -> Void
  38. var content: Content
  39. init(
  40. tableDeletionState: Binding<TableDeletionState>,
  41. index: Int,
  42. isDeletable: Bool,
  43. onDelete delete: @escaping () -> Void,
  44. @ViewBuilder content: () -> Content
  45. ) {
  46. self._tableDeletionState = tableDeletionState
  47. self.index = index
  48. self.isDeletable = isDeletable
  49. self.delete = delete
  50. self.content = content()
  51. }
  52. var body: some View {
  53. HStack(spacing: 16) {
  54. if isDeletable &&
  55. tableDeletionState != .disabled &&
  56. tableDeletionState.indexAwaitingDeletionConfirmation != index
  57. {
  58. DeletionIndicator()
  59. .transition(AnyTransition.move(edge: .leading).combined(with: .opacity))
  60. .onTapGesture {
  61. withAnimation {
  62. self.tableDeletionState = .awaitingConfirmation(toDeleteItemAt: self.index)
  63. }
  64. }
  65. }
  66. HStack(spacing: 16) {
  67. content
  68. .disabled(tableDeletionState != .disabled)
  69. .contentShape(Rectangle())
  70. .onTapGesture {
  71. if self.tableDeletionState.isAwaitingConfirmation {
  72. withAnimation {
  73. self.tableDeletionState = .enabled
  74. }
  75. }
  76. }
  77. if tableDeletionState.indexAwaitingDeletionConfirmation == index {
  78. Text(LocalizedString("Delete", comment: "Test for table cell delete button"))
  79. .lineLimit(1)
  80. .foregroundColor(.white)
  81. .background(
  82. // Expand into margins
  83. Color.red.padding(-12)
  84. )
  85. .transition(AnyTransition.move(edge: .trailing).combined(with: .opacity))
  86. .offset(x: 4) // Push into margin
  87. .onTapGesture {
  88. withAnimation {
  89. self.tableDeletionState = .enabled
  90. self.delete()
  91. }
  92. }
  93. }
  94. }
  95. }
  96. }
  97. }
  98. private struct DeletionIndicator: View {
  99. var body: some View {
  100. Text("-")
  101. .bold()
  102. .foregroundColor(.white)
  103. .padding(1)
  104. .background(Circle().fill(Color.red))
  105. .padding(-1) // Prevent circle background from affecting layout
  106. }
  107. }