Alert.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. //
  2. // Alert.swift
  3. // LoopKit
  4. //
  5. // Created by Rick Pasetto on 4/8/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. /// Protocol that describes any class that presents Alerts.
  10. public protocol AlertPresenter: AnyObject {
  11. /// Issue (post) the given alert, according to its trigger schedule.
  12. func issueAlert(_ alert: Alert)
  13. /// Retract any alerts with the given identifier. This includes both pending and delivered alerts.
  14. func retractAlert(identifier: Alert.Identifier)
  15. }
  16. /// Protocol that describes something that can deal with a user's response to an alert.
  17. public protocol AlertResponder: AnyObject {
  18. /// Acknowledge alerts with a given type identifier
  19. func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier) -> Void
  20. }
  21. /// Structure that represents an Alert that is issued from a Device.
  22. public struct Alert: Equatable {
  23. /// Representation of an alert Trigger
  24. public enum Trigger: Equatable {
  25. /// Trigger the alert immediately
  26. case immediate
  27. /// Delay triggering the alert by `interval`, but issue it only once.
  28. case delayed(interval: TimeInterval)
  29. /// Delay triggering the alert by `repeatInterval`, and repeat at that interval until cancelled or unscheduled.
  30. case repeating(repeatInterval: TimeInterval)
  31. }
  32. /// Content of the alert, either for foreground or background alerts
  33. public struct Content: Equatable {
  34. public let title: String
  35. public let body: String
  36. /// Should this alert be deemed "critical" for the User? Handlers will determine how that is manifested.
  37. public let isCritical: Bool
  38. // TODO: when we have more complicated actions. For now, all we have is "acknowledge".
  39. // let actions: [UserAlertAction]
  40. public let acknowledgeActionButtonLabel: String
  41. public init(title: String, body: String, acknowledgeActionButtonLabel: String, isCritical: Bool = false) {
  42. self.title = title
  43. self.body = body
  44. self.acknowledgeActionButtonLabel = acknowledgeActionButtonLabel
  45. self.isCritical = isCritical
  46. }
  47. }
  48. public struct Identifier: Equatable, Hashable {
  49. /// Unique device manager identifier from whence the alert came, and to which alert acknowledgements should be directed.
  50. public let managerIdentifier: String
  51. /// Per-alert-type identifier, for instance to group alert types. This is the identifier that will be used to acknowledge the alert.
  52. public let alertIdentifier: AlertIdentifier
  53. public init(managerIdentifier: String, alertIdentifier: AlertIdentifier) {
  54. self.managerIdentifier = managerIdentifier
  55. self.alertIdentifier = alertIdentifier
  56. }
  57. /// An opaque value for this tuple for unique identification of the alert across devices.
  58. public var value: String {
  59. return "\(managerIdentifier).\(alertIdentifier)"
  60. }
  61. }
  62. /// This type represents a per-alert-type identifier, but not necessarily unique across devices. Each device may have its own Swift type for this,
  63. /// so conversion to String is the most convenient, but aliasing the type is helpful because it is not just "any String".
  64. public typealias AlertIdentifier = String
  65. /// Alert content to show while app is in the foreground. If nil, there shall be no alert while app is in the foreground.
  66. public let foregroundContent: Content?
  67. /// Alert content to show while app is in the background. If nil, there shall be no alert while app is in the background.
  68. public let backgroundContent: Content?
  69. /// Trigger for the alert.
  70. public let trigger: Trigger
  71. /// An alert's "identifier" is a tuple of `managerIdentifier` and `alertIdentifier`. It's purpose is to uniquely identify an alert so we can
  72. /// find which device issued it, and send acknowledgment of that alert to the proper device manager.
  73. public let identifier: Identifier
  74. /// Representation of a "sound" (or other sound-like action, like vibrate) to perform when the alert is issued.
  75. public enum Sound: Equatable {
  76. case vibrate
  77. case silence
  78. case sound(name: String)
  79. }
  80. public let sound: Sound?
  81. public init(identifier: Identifier, foregroundContent: Content?, backgroundContent: Content?, trigger: Trigger, sound: Sound? = nil) {
  82. self.identifier = identifier
  83. self.foregroundContent = foregroundContent
  84. self.backgroundContent = backgroundContent
  85. self.trigger = trigger
  86. self.sound = sound
  87. }
  88. }
  89. public extension Alert.Sound {
  90. var filename: String? {
  91. switch self {
  92. case .sound(let name): return name
  93. case .silence, .vibrate: return nil
  94. }
  95. }
  96. }
  97. public protocol AlertSoundVendor {
  98. // Get the base URL for where to find all the vendor's sounds. It is under here that all of the sound files should be.
  99. // Returns nil if the vendor has no sounds.
  100. func getSoundBaseURL() -> URL?
  101. // Get all the sounds for this vendor. Returns an empty array if the vendor has no sounds.
  102. func getSounds() -> [Alert.Sound]
  103. }
  104. // MARK: Codable implementations
  105. extension Alert: Codable { }
  106. extension Alert.Content: Codable { }
  107. extension Alert.Identifier: Codable { }
  108. // These Codable implementations of enums with associated values cannot be synthesized (yet) in Swift.
  109. // The code below follows a pattern described by https://medium.com/@hllmandel/codable-enum-with-associated-values-swift-4-e7d75d6f4370
  110. extension Alert.Trigger: Codable {
  111. private enum CodingKeys: String, CodingKey {
  112. case immediate, delayed, repeating
  113. }
  114. private struct Delayed: Codable {
  115. let delayInterval: TimeInterval
  116. }
  117. private struct Repeating: Codable {
  118. let repeatInterval: TimeInterval
  119. }
  120. public init(from decoder: Decoder) throws {
  121. if let singleValue = try? decoder.singleValueContainer().decode(CodingKeys.RawValue.self) {
  122. switch singleValue {
  123. case CodingKeys.immediate.rawValue:
  124. self = .immediate
  125. default:
  126. throw decoder.enumDecodingError
  127. }
  128. } else {
  129. let container = try decoder.container(keyedBy: CodingKeys.self)
  130. if let delayInterval = try? container.decode(Delayed.self, forKey: .delayed) {
  131. self = .delayed(interval: delayInterval.delayInterval)
  132. } else if let repeatInterval = try? container.decode(Repeating.self, forKey: .repeating) {
  133. self = .repeating(repeatInterval: repeatInterval.repeatInterval)
  134. } else {
  135. throw decoder.enumDecodingError
  136. }
  137. }
  138. }
  139. public func encode(to encoder: Encoder) throws {
  140. switch self {
  141. case .immediate:
  142. var container = encoder.singleValueContainer()
  143. try container.encode(CodingKeys.immediate.rawValue)
  144. case .delayed(let interval):
  145. var container = encoder.container(keyedBy: CodingKeys.self)
  146. try container.encode(Delayed(delayInterval: interval), forKey: .delayed)
  147. case .repeating(let repeatInterval):
  148. var container = encoder.container(keyedBy: CodingKeys.self)
  149. try container.encode(Repeating(repeatInterval: repeatInterval), forKey: .repeating)
  150. }
  151. }
  152. }
  153. extension Alert.Sound: Codable {
  154. private enum CodingKeys: String, CodingKey {
  155. case silence, vibrate, sound
  156. }
  157. private struct SoundName: Codable {
  158. let name: String
  159. }
  160. public init(from decoder: Decoder) throws {
  161. if let singleValue = try? decoder.singleValueContainer().decode(CodingKeys.RawValue.self) {
  162. switch singleValue {
  163. case CodingKeys.silence.rawValue:
  164. self = .silence
  165. case CodingKeys.vibrate.rawValue:
  166. self = .vibrate
  167. default:
  168. throw decoder.enumDecodingError
  169. }
  170. } else {
  171. let container = try decoder.container(keyedBy: CodingKeys.self)
  172. if let name = try? container.decode(SoundName.self, forKey: .sound) {
  173. self = .sound(name: name.name); return
  174. } else {
  175. throw decoder.enumDecodingError
  176. }
  177. }
  178. }
  179. public func encode(to encoder: Encoder) throws {
  180. switch self {
  181. case .silence:
  182. var container = encoder.singleValueContainer()
  183. try container.encode(CodingKeys.silence.rawValue)
  184. case .vibrate:
  185. var container = encoder.singleValueContainer()
  186. try container.encode(CodingKeys.vibrate.rawValue)
  187. case .sound(let name):
  188. var container = encoder.container(keyedBy: CodingKeys.self)
  189. try container.encode(SoundName(name: name), forKey: .sound)
  190. }
  191. }
  192. }
  193. extension Decoder {
  194. var enumDecodingError: DecodingError {
  195. return DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "invalid enumeration"))
  196. }
  197. }