iOS-study
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SwiftUIToast.swift 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. //
  2. // SwiftUIToast.swift
  3. // iOSFirst
  4. //
  5. // Created by 孙宇峰 on 2023/2/2.
  6. //
  7. import SwiftUI
  8. /**
  9. SUIToast
  10. Alias of SUIToastController
  11. */
  12. public var SUIToast : SUIToastController {
  13. return SUIToastController.shared
  14. }
  15. /**
  16. SUIToastController
  17. Supposedly a singleton to wrap and control all toast items
  18. */
  19. public class SUIToastController : ObservableObject {
  20. public static var shared = SUIToastController()
  21. public var toastLengthLong = 3.5
  22. public var toastLengthShort = 2.0
  23. @Published var items: [SUIToastViewCellItem] = []
  24. private var updateTimer: Timer?
  25. func initialize() {
  26. startTimer()
  27. }
  28. func uninitialize() {
  29. stopTimer()
  30. }
  31. public func show(_ message: String) {
  32. items.append(
  33. .init(
  34. message: message
  35. )
  36. )
  37. }
  38. public func show(messageItem: SUIToastViewCellItem) {
  39. items.append(
  40. messageItem
  41. )
  42. }
  43. func startTimer() {
  44. updateTimer = .scheduledTimer(
  45. timeInterval: 0.05,
  46. target: self,
  47. selector: #selector(SUIToastController.onUpdate),
  48. userInfo: nil,
  49. repeats: true)
  50. }
  51. func stopTimer() {
  52. if let updateTimer = updateTimer {
  53. updateTimer.invalidate()
  54. self.updateTimer = nil
  55. }
  56. }
  57. @objc func onUpdate() {
  58. var isUpdated = false
  59. for k in (0 ..< items.count).reversed() {
  60. let it = items[k]
  61. if it.isExpired {
  62. items.remove(at: k)
  63. isUpdated = true
  64. }
  65. }
  66. if isUpdated {
  67. self.objectWillChange.send()
  68. }
  69. }
  70. }
  71. public struct SUIToastViewCellItem: Identifiable, Hashable {
  72. public var id: String
  73. public var message: String
  74. public var bgColor: Color
  75. public var messageColor: Color
  76. public var createdAt: Date
  77. public var toastLength: CGFloat
  78. public init(
  79. message: String = "",
  80. length: CGFloat = SUIToast.toastLengthLong,
  81. bgColor: Color = .black,
  82. messageColor: Color = .white
  83. ) {
  84. self.id = UUID().uuidString
  85. self.message = message
  86. self.bgColor = bgColor
  87. self.messageColor = messageColor
  88. self.createdAt = Date.now
  89. self.toastLength = length
  90. }
  91. var isExpired: Bool {
  92. get {
  93. return Date.now.timeIntervalSinceReferenceDate - createdAt.timeIntervalSinceReferenceDate > toastLength
  94. }
  95. }
  96. }
  97. public struct SUIToastViewContainer: View {
  98. @StateObject var toastObs = SUIToastController.shared
  99. public enum StackAlignment {
  100. case top
  101. case bottom
  102. case middle
  103. }
  104. public enum StackOverlap {
  105. case overlap
  106. case stack
  107. }
  108. var stackAlignment: StackAlignment = .bottom
  109. var stackOverlap: StackOverlap = .overlap
  110. public init() {
  111. }
  112. public init(
  113. stackAlignment: StackAlignment = .bottom,
  114. stackOverlap: StackOverlap = .overlap,
  115. toastObs: StateObject<SUIToastController>? = nil
  116. ) {
  117. self.stackAlignment = stackAlignment
  118. self.stackOverlap = stackOverlap
  119. if let toastObs = toastObs {
  120. self._toastObs = toastObs
  121. }
  122. }
  123. public var body: some View {
  124. ZStack {
  125. VStack(spacing: 0) {
  126. if stackAlignment == .bottom
  127. || stackAlignment == .middle {
  128. Spacer()
  129. }
  130. if stackOverlap == .overlap {
  131. ZStack(alignment: .center) {
  132. ForEach(
  133. toastObs.items,
  134. id: \.id
  135. )
  136. { it in
  137. VStack(spacing: 0) {
  138. Spacer()
  139. SUIToastViewCell(
  140. item: it
  141. )
  142. .zIndex(it.createdAt.timeIntervalSinceReferenceDate)
  143. .transition(
  144. .opacity.combined(with: .move(edge: .bottom))
  145. )
  146. }
  147. }
  148. }
  149. }
  150. else if stackOverlap == .stack
  151. {
  152. ForEach(
  153. toastObs.items.reversed(),
  154. id: \.id
  155. )
  156. { it in
  157. SUIToastViewCell(
  158. item: it
  159. )
  160. .transition(
  161. .opacity.combined(with: .move(edge: .bottom))
  162. )
  163. }
  164. }
  165. if stackAlignment == .top
  166. || stackAlignment == .middle {
  167. Spacer()
  168. }
  169. }
  170. }
  171. .animation(.default, value: toastObs.items)
  172. .onAppear {
  173. toastObs.initialize()
  174. }
  175. .onDisappear {
  176. toastObs.uninitialize()
  177. }
  178. }
  179. }
  180. internal struct SUIToastViewCell: View {
  181. var item: SUIToastViewCellItem
  182. var body: some View {
  183. ZStack {
  184. VStack(spacing: 0) {
  185. HStack(spacing: 8) {
  186. Text(item.message)
  187. .foregroundColor(item.messageColor)
  188. }
  189. }
  190. .padding(.horizontal, 20)
  191. .padding(.vertical, 10)
  192. .background(
  193. RoundedRectangle(cornerRadius: 10)
  194. .fill(item.bgColor)
  195. )
  196. }
  197. .padding(.horizontal, 20)
  198. .padding(.vertical, 10)
  199. .allowsHitTesting(false)
  200. }
  201. }