Alert.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 issues and retract Alerts.
  10. public protocol AlertIssuer: 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. If the alert fails to clear, an error should be passed to the completion handler, indicating the cause of failure.
  19. func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) -> Void
  20. }
  21. public struct PersistedAlert: Equatable {
  22. public let alert: Alert
  23. public let issuedDate: Date
  24. public let retractedDate: Date?
  25. public let acknowledgedDate: Date?
  26. public init(alert: Alert, issuedDate: Date, retractedDate: Date?, acknowledgedDate: Date?) {
  27. self.alert = alert
  28. self.issuedDate = issuedDate
  29. self.retractedDate = retractedDate
  30. self.acknowledgedDate = acknowledgedDate
  31. }
  32. }
  33. /// Protocol for recording and looking up alerts persisted in storage
  34. public protocol PersistedAlertStore {
  35. /// Determine if an alert is already issued for a given `Alert.Identifier`.
  36. func doesIssuedAlertExist(identifier: Alert.Identifier, completion: @escaping (Swift.Result<Bool, Error>) -> Void)
  37. /// Look up all issued, but unretracted, alerts for a given `managerIdentifier`. This is useful for an Alert issuer to see what alerts are extant (outstanding).
  38. /// NOTE: the completion function may be called on a different queue than the caller. Callers must be prepared for this.
  39. func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void)
  40. /// Look up all issued, but unretracted, and unacknowledged, alerts for a given `managerIdentifier`. This is useful for an Alert issuer to see what alerts are extant (outstanding).
  41. /// NOTE: the completion function may be called on a different queue than the caller. Callers must be prepared for this.
  42. func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void)
  43. /// Records an alert that occurred (likely in the past) but is already retracted. This alert will never be presented to the user by an AlertPresenter. Such a retracted alert has the same date for issued and retracted dates, and there is no acknowledged date
  44. func recordRetractedAlert(_ alert: Alert, at date: Date)
  45. }
  46. /// Structure that represents an Alert that is issued from a Device.
  47. public struct Alert: Equatable {
  48. /// Representation of an alert Trigger
  49. public enum Trigger: Equatable {
  50. /// Trigger the alert immediately
  51. case immediate
  52. /// Delay triggering the alert by `interval`, but issue it only once.
  53. case delayed(interval: TimeInterval)
  54. /// Delay triggering the alert by `repeatInterval`, and repeat at that interval until cancelled or unscheduled.
  55. case repeating(repeatInterval: TimeInterval)
  56. }
  57. /// The interruption level of the alert. Note that these follow the same definitions as defined by https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel
  58. /// Handlers will determine how that is manifested.
  59. public enum InterruptionLevel: String {
  60. /// The system presents the notification immediately, lights up the screen, and can play a sound. These alerts may be deferred if the user chooses.
  61. case active
  62. /// The system presents the notification immediately, lights up the screen, and can play a sound. These alerts may not be deferred.
  63. case timeSensitive
  64. /// The system makes every attempt at alerting the user, including (possibly) ignoring the mute switch, or the user's notification settings.
  65. case critical
  66. }
  67. /// Content of the alert, either for foreground or background alerts
  68. public struct Content: Equatable {
  69. public let title: String
  70. public let body: String
  71. // TODO: when we have more complicated actions. For now, all we have is "acknowledge".
  72. // let actions: [UserAlertAction]
  73. public let acknowledgeActionButtonLabel: String
  74. public init(title: String, body: String, acknowledgeActionButtonLabel: String) {
  75. self.title = title
  76. self.body = body
  77. self.acknowledgeActionButtonLabel = acknowledgeActionButtonLabel
  78. }
  79. }
  80. public struct Identifier: Equatable, Hashable {
  81. /// Unique device manager identifier from whence the alert came, and to which alert acknowledgements should be directed.
  82. public let managerIdentifier: String
  83. /// Per-alert-type identifier, for instance to group alert types. This is the identifier that will be used to acknowledge the alert.
  84. public let alertIdentifier: AlertIdentifier
  85. public init(managerIdentifier: String, alertIdentifier: AlertIdentifier) {
  86. self.managerIdentifier = managerIdentifier
  87. self.alertIdentifier = alertIdentifier
  88. }
  89. /// An opaque value for this tuple for unique identification of the alert across devices.
  90. public var value: String {
  91. return "\(managerIdentifier).\(alertIdentifier)"
  92. }
  93. }
  94. /// This type represents a per-alert-type identifier, but not necessarily unique across devices. Each device may have its own Swift type for this,
  95. /// so conversion to String is the most convenient, but aliasing the type is helpful because it is not just "any String".
  96. public typealias AlertIdentifier = String
  97. /// Alert content to show while app is in the foreground. If nil, there shall be no alert while app is in the foreground.
  98. public let foregroundContent: Content?
  99. /// Alert content to show while app is in the background.
  100. public let backgroundContent: Content
  101. /// Trigger for the alert.
  102. public let trigger: Trigger
  103. /// Interruption level for the alert. See `InterruptionLevel` above.
  104. public let interruptionLevel: InterruptionLevel
  105. /// An alert's "identifier" is a tuple of `managerIdentifier` and `alertIdentifier`. It's purpose is to uniquely identify an alert so we can
  106. /// find which device issued it, and send acknowledgment of that alert to the proper device manager.
  107. public let identifier: Identifier
  108. /// Representation of a "sound" (or other sound-like action, like vibrate) to perform when the alert is issued.
  109. public enum Sound: Equatable {
  110. case vibrate
  111. case sound(name: String)
  112. }
  113. public let sound: Sound?
  114. /// Any metadata for the alert used to customize the alert content
  115. public typealias MetadataValue = AnyCodableEquatable
  116. public typealias Metadata = [String: MetadataValue]
  117. public let metadata: Metadata?
  118. public init(identifier: Identifier,
  119. foregroundContent: Content?,
  120. backgroundContent: Content,
  121. trigger: Trigger,
  122. interruptionLevel: InterruptionLevel = .timeSensitive,
  123. sound: Sound? = nil,
  124. metadata: Metadata? = nil)
  125. {
  126. self.identifier = identifier
  127. self.foregroundContent = foregroundContent
  128. self.backgroundContent = backgroundContent
  129. self.trigger = trigger
  130. self.interruptionLevel = interruptionLevel
  131. self.sound = sound
  132. self.metadata = metadata
  133. }
  134. }
  135. public extension Alert.Sound {
  136. var filename: String? {
  137. switch self {
  138. case .sound(let name): return name
  139. case .vibrate: return nil
  140. }
  141. }
  142. }
  143. public protocol AlertSoundVendor {
  144. // 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.
  145. // Returns nil if the vendor has no sounds.
  146. func getSoundBaseURL() -> URL?
  147. // Get all the sounds for this vendor. Returns an empty array if the vendor has no sounds.
  148. func getSounds() -> [Alert.Sound]
  149. }
  150. // MARK: Codable implementations
  151. extension Alert: Codable { }
  152. extension Alert.Content: Codable { }
  153. extension Alert.Identifier: Codable { }
  154. extension Alert.InterruptionLevel: Codable { }
  155. // These Codable implementations of enums with associated values cannot be synthesized (yet) in Swift.
  156. // The code below follows a pattern described by https://medium.com/@hllmandel/codable-enum-with-associated-values-swift-4-e7d75d6f4370
  157. extension Alert.Trigger: Codable {
  158. private enum CodingKeys: String, CodingKey {
  159. case immediate, delayed, repeating
  160. }
  161. private struct Delayed: Codable {
  162. let delayInterval: TimeInterval
  163. }
  164. private struct Repeating: Codable {
  165. let repeatInterval: TimeInterval
  166. }
  167. public init(from decoder: Decoder) throws {
  168. if let singleValue = try? decoder.singleValueContainer().decode(CodingKeys.RawValue.self) {
  169. switch singleValue {
  170. case CodingKeys.immediate.rawValue:
  171. self = .immediate
  172. default:
  173. throw decoder.enumDecodingError
  174. }
  175. } else {
  176. let container = try decoder.container(keyedBy: CodingKeys.self)
  177. if let delayInterval = try? container.decode(Delayed.self, forKey: .delayed) {
  178. self = .delayed(interval: delayInterval.delayInterval)
  179. } else if let repeatInterval = try? container.decode(Repeating.self, forKey: .repeating) {
  180. self = .repeating(repeatInterval: repeatInterval.repeatInterval)
  181. } else {
  182. throw decoder.enumDecodingError
  183. }
  184. }
  185. }
  186. public func encode(to encoder: Encoder) throws {
  187. switch self {
  188. case .immediate:
  189. var container = encoder.singleValueContainer()
  190. try container.encode(CodingKeys.immediate.rawValue)
  191. case .delayed(let interval):
  192. var container = encoder.container(keyedBy: CodingKeys.self)
  193. try container.encode(Delayed(delayInterval: interval), forKey: .delayed)
  194. case .repeating(let repeatInterval):
  195. var container = encoder.container(keyedBy: CodingKeys.self)
  196. try container.encode(Repeating(repeatInterval: repeatInterval), forKey: .repeating)
  197. }
  198. }
  199. }
  200. extension Alert.Sound: Codable {
  201. private enum CodingKeys: String, CodingKey {
  202. case vibrate, sound
  203. }
  204. private struct SoundName: Codable {
  205. let name: String
  206. }
  207. public init(from decoder: Decoder) throws {
  208. if let singleValue = try? decoder.singleValueContainer().decode(CodingKeys.RawValue.self) {
  209. switch singleValue {
  210. case CodingKeys.vibrate.rawValue:
  211. self = .vibrate
  212. default:
  213. throw decoder.enumDecodingError
  214. }
  215. } else {
  216. let container = try decoder.container(keyedBy: CodingKeys.self)
  217. if let name = try? container.decode(SoundName.self, forKey: .sound) {
  218. self = .sound(name: name.name); return
  219. } else {
  220. throw decoder.enumDecodingError
  221. }
  222. }
  223. }
  224. public func encode(to encoder: Encoder) throws {
  225. switch self {
  226. case .vibrate:
  227. var container = encoder.singleValueContainer()
  228. try container.encode(CodingKeys.vibrate.rawValue)
  229. case .sound(let name):
  230. var container = encoder.container(keyedBy: CodingKeys.self)
  231. try container.encode(SoundName(name: name), forKey: .sound)
  232. }
  233. }
  234. }
  235. public extension Alert.Metadata {
  236. init<E: Codable & Equatable>(dict: [String: E]) {
  237. self = dict.mapValues { Alert.MetadataValue($0) }
  238. }
  239. }
  240. extension Decoder {
  241. var enumDecodingError: DecodingError {
  242. return DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "invalid enumeration"))
  243. }
  244. }