Skip to content

Instantly share code, notes, and snippets.

@Miiha
Last active July 14, 2021 06:25
Show Gist options
  • Save Miiha/5b26556e2d24ef190cef5fe383cba8c4 to your computer and use it in GitHub Desktop.
Save Miiha/5b26556e2d24ef190cef5fe383cba8c4 to your computer and use it in GitHub Desktop.
UNUserNotificationCenter modelled with simple data types using pointfree.co's approach of designing dependencies.
import Foundation
import UserNotifications
import Combine
import CoreLocation
public struct UserNotificationClient {
public var add: (UNNotificationRequest) -> AnyPublisher<Void, Error>
public var getAuthStatus: () -> AnyPublisher<UNAuthorizationStatus, Never>
public var getDeliveredNotifications: () -> AnyPublisher<[Notification], Never>
public var getNotificationCategories: () -> AnyPublisher<Set<UNNotificationCategory>, Never>
public var getNotificationSettings: () -> AnyPublisher<NotificationSettings, Never>
public var getPendingNotificationRequests: () -> AnyPublisher<[NotificationRequest], Never>
public var removeAllDeliveredNotifications: () -> Void
public var removeAllPendingNotificationRequests: () -> Void
public var removeDeliveredNotifications: ([String]) -> Void
public var removePendingNotificationRequests: ([String]) -> Void
public var requestAuthorization: (UNAuthorizationOptions) -> AnyPublisher<Bool, NSError>
public var setNotificationCategories: (Set<UNNotificationCategory>) -> Void
public var supportsContentExtensions: () -> Bool
public var delegate: AnyPublisher<DelegateEvent, Never>
public init(
add: @escaping (UNNotificationRequest) -> AnyPublisher<Void, Error>,
getAuthStatus: @escaping () -> AnyPublisher<UNAuthorizationStatus, Never>,
getDeliveredNotifications: @escaping () -> AnyPublisher<[Notification], Never>,
getNotificationSettings: @escaping () -> AnyPublisher<NotificationSettings, Never>,
getNotificationCategories: @escaping () -> AnyPublisher<Set<UNNotificationCategory>, Never>,
getPendingNotificationRequests: @escaping () -> AnyPublisher<[NotificationRequest], Never>,
removeAllDeliveredNotifications: @escaping () -> Void,
removeAllPendingNotificationRequests: @escaping () -> Void,
removeDeliveredNotifications: @escaping ([String]) -> Void,
removePendingNotificationRequests: @escaping ([String]) -> Void,
requestAuthorization: @escaping (UNAuthorizationOptions) -> AnyPublisher<Bool, NSError>,
setNotificationCategories: @escaping (Set<UNNotificationCategory>) -> Void,
supportsContentExtensions: @escaping () -> Bool,
delegate: AnyPublisher<DelegateEvent, Never>
) {
self.add = add
self.getAuthStatus = getAuthStatus
self.getDeliveredNotifications = getDeliveredNotifications
self.getNotificationSettings = getNotificationSettings
self.getNotificationCategories = getNotificationCategories
self.getPendingNotificationRequests = getPendingNotificationRequests
self.removeAllDeliveredNotifications = removeAllDeliveredNotifications
self.removeAllPendingNotificationRequests = removeAllPendingNotificationRequests
self.removeDeliveredNotifications = removeDeliveredNotifications
self.removePendingNotificationRequests = removePendingNotificationRequests
self.requestAuthorization = requestAuthorization
self.setNotificationCategories = setNotificationCategories
self.supportsContentExtensions = supportsContentExtensions
self.delegate = delegate
}
public enum DelegateEvent {
case willPresentNotification(
_ notification: Notification,
completion: (UNNotificationPresentationOptions) -> Void)
case didReceiveResponse(_ response: NotificationResponseType, completion: () -> Void)
case openSettingsForNotification(_ notification: Notification?)
}
}
public struct Notification {
public let rawValue: UNNotification?
public var date: Date
public var request: NotificationRequest
public init(rawValue: UNNotification) {
self.rawValue = rawValue
self.date = rawValue.date
self.request = NotificationRequest(rawValue: rawValue.request)
}
}
public struct NotificationRequest {
public let rawValue: UNNotificationRequest?
public var identifier: String
public var content: NotificationContent
public var trigger: NotificationTrigger?
public init(rawValue: UNNotificationRequest) {
self.rawValue = rawValue
self.identifier = rawValue.identifier
self.content = NotificationContent(rawValue: rawValue.content)
self.trigger = {
switch rawValue.trigger {
case let trigger as UNPushNotificationTrigger:
return PushNotificationTrigger(rawValue: trigger)
case let trigger as UNCalendarNotificationTrigger:
return CalendarNotificationTrigger(rawValue: trigger)
case let trigger as UNLocationNotificationTrigger:
return LocationNotificationTrigger(rawValue: trigger)
default:
return nil
}
}()
}
}
public struct NotificationContent {
public let rawValue: UNNotificationContent?
public var title: String
public var subtitle: String
public var body: String
public var badge: NSNumber?
public var sound: UNNotificationSound?
public var launchImageName: String
public var userInfo: [AnyHashable : Any]
public var attachments: [NotificationAttachment]
public var summaryArgument: String
public var summaryArgumentCount: Int
public var categoryIdentifier: String
public var threadIdentifier: String
public var targetContentIdentifier: String?
public init(rawValue: UNNotificationContent) {
self.rawValue = rawValue
self.title = rawValue.title
self.subtitle = rawValue.subtitle
self.body = rawValue.body
self.badge = rawValue.badge
self.sound = rawValue.sound
self.launchImageName = rawValue.launchImageName
self.userInfo = rawValue.userInfo
self.attachments = rawValue.attachments.map(NotificationAttachment.init)
self.summaryArgument = rawValue.summaryArgument
self.summaryArgumentCount = rawValue.summaryArgumentCount
self.categoryIdentifier = rawValue.categoryIdentifier
self.threadIdentifier = rawValue.threadIdentifier
self.targetContentIdentifier = rawValue.targetContentIdentifier
}
}
public struct NotificationAttachment: Equatable {
public let rawValue: UNNotificationAttachment
public var identifier: String
public var url: URL
public var type: String
public init(rawValue: UNNotificationAttachment) {
self.rawValue = rawValue
self.identifier = rawValue.identifier
self.url = rawValue.url
self.type = rawValue.type
}
}
public protocol NotificationTrigger {
var repeats: Bool { get }
}
public struct PushNotificationTrigger: NotificationTrigger {
public let repeats: Bool
public init(rawValue: UNPushNotificationTrigger) {
self.repeats = rawValue.repeats
}
}
public struct TimeIntervalNotificationTrigger: NotificationTrigger {
public let rawValue: UNTimeIntervalNotificationTrigger?
public var repeats: Bool
public var timeInterval: TimeInterval
public var nextTriggerDate: () -> Date?
init(rawValue: UNTimeIntervalNotificationTrigger) {
self.rawValue = rawValue
self.repeats = rawValue.repeats
self.timeInterval = rawValue.timeInterval
self.nextTriggerDate = rawValue.nextTriggerDate
}
public static func == (lhs: TimeIntervalNotificationTrigger, rhs: TimeIntervalNotificationTrigger) -> Bool {
lhs.repeats == rhs.repeats && lhs.timeInterval == rhs.timeInterval
}
}
public struct CalendarNotificationTrigger: NotificationTrigger {
public let rawValue: UNCalendarNotificationTrigger?
public var repeats: Bool
public var dateComponents: DateComponents
public var nextTriggerDate: () -> Date?
init(rawValue: UNCalendarNotificationTrigger) {
self.rawValue = rawValue
self.repeats = rawValue.repeats
self.dateComponents = rawValue.dateComponents
self.nextTriggerDate = rawValue.nextTriggerDate
}
public static func == (lhs: CalendarNotificationTrigger, rhs: CalendarNotificationTrigger) -> Bool {
lhs.repeats == rhs.repeats && lhs.dateComponents == rhs.dateComponents
}
}
public struct LocationNotificationTrigger: NotificationTrigger, Equatable {
public let rawValue: UNLocationNotificationTrigger?
public var repeats: Bool
public var region: Region
init(rawValue: UNLocationNotificationTrigger) {
self.rawValue = rawValue
self.repeats = rawValue.repeats
self.region = Region(rawValue: rawValue.region)
}
}
public protocol NotificationResponseType {
var actionIdentifier: String { get }
var notification: Notification { get }
}
public struct NotificationResponse: NotificationResponseType {
public let rawValue: UNNotificationResponse?
public var actionIdentifier: String
public var notification: Notification
public init(rawValue: UNNotificationResponse) {
self.rawValue = rawValue
self.actionIdentifier = rawValue.actionIdentifier
self.notification = Notification(rawValue: rawValue.notification)
}
}
public struct TextInputNotificationResponse: NotificationResponseType {
public let rawValue: UNTextInputNotificationResponse?
public var actionIdentifier: String
public var notification: Notification
public var userText: String
public init(rawValue: UNTextInputNotificationResponse) {
self.rawValue = rawValue
self.actionIdentifier = rawValue.actionIdentifier
self.notification = Notification(rawValue: rawValue.notification)
self.userText = rawValue.userText
}
}
public struct NotificationSettings {
public let rawValue: UNNotificationSettings?
public var alertSetting: UNNotificationSetting
public var alertStyle: UNAlertStyle
public var announcementSetting: UNNotificationSetting
public var authorizationStatus: UNAuthorizationStatus
public var badgeSetting: UNNotificationSetting
public var carPlaySetting: UNNotificationSetting
public var criticalAlertSetting: UNNotificationSetting
public var lockScreenSetting: UNNotificationSetting
public var notificationCenterSetting: UNNotificationSetting
public var providesAppNotificationSettings: Bool
public var showPreviewsSetting: UNShowPreviewsSetting
public var soundSetting: UNNotificationSetting
public init(rawValue: UNNotificationSettings) {
self.rawValue = rawValue
self.alertSetting = rawValue.alertSetting
self.alertStyle = rawValue.alertStyle
self.announcementSetting = rawValue.announcementSetting
self.authorizationStatus = rawValue.authorizationStatus
self.badgeSetting = rawValue.badgeSetting
self.carPlaySetting = rawValue.carPlaySetting
self.criticalAlertSetting = rawValue.criticalAlertSetting
self.lockScreenSetting = rawValue.lockScreenSetting
self.notificationCenterSetting = rawValue.notificationCenterSetting
self.providesAppNotificationSettings = rawValue.providesAppNotificationSettings
self.showPreviewsSetting = rawValue.showPreviewsSetting
self.soundSetting = rawValue.soundSetting
}
}
// see https://github.com/pointfreeco/swift-composable-architecture/blob/767e1d9553fcee5a95af10e0352f20fb03b98352/Sources/ComposableCoreLocation/Models/Region.swift#L5
public struct Region: Hashable {
public let rawValue: CLRegion?
public var identifier: String
public var notifyOnEntry: Bool
public var notifyOnExit: Bool
init(rawValue: CLRegion) {
self.rawValue = rawValue
self.identifier = rawValue.identifier
self.notifyOnEntry = rawValue.notifyOnEntry
self.notifyOnExit = rawValue.notifyOnExit
}
init(
identifier: String,
notifyOnEntry: Bool,
notifyOnExit: Bool
) {
self.rawValue = nil
self.identifier = identifier
self.notifyOnEntry = notifyOnEntry
self.notifyOnExit = notifyOnExit
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.identifier == rhs.identifier
&& lhs.notifyOnEntry == rhs.notifyOnEntry
&& lhs.notifyOnExit == rhs.notifyOnExit
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.identifier)
hasher.combine(self.notifyOnExit)
hasher.combine(self.notifyOnEntry)
}
}
import Foundation
import UserNotifications
import Combine
extension UserNotificationClient {
public static var live: UserNotificationClient {
final class Delegate: NSObject, UNUserNotificationCenterDelegate {
let subject: PassthroughSubject<DelegateEvent, Never>
init(subject: PassthroughSubject<DelegateEvent, Never>) {
self.subject = subject
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
subject.send(
.willPresentNotification(
Notification(rawValue: notification),
completion: completionHandler)
)
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let mappedResponse: NotificationResponseType = {
switch response {
case let response as UNTextInputNotificationResponse:
return TextInputNotificationResponse(rawValue: response)
default:
return NotificationResponse(rawValue: response)
}
}()
subject.send(.didReceiveResponse(mappedResponse, completion: completionHandler))
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?) {
let mappedNotification = notification.map(Notification.init)
subject.send(.openSettingsForNotification(mappedNotification))
}
}
let center = UNUserNotificationCenter.current()
let subject = PassthroughSubject<DelegateEvent, Never>()
var delegate: Delegate? = Delegate(subject: subject)
center.delegate = delegate
return Self(
add: { request in
Future { promise in
center.add(request) { error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(()))
}
}
}.eraseToAnyPublisher()
},
getAuthStatus: {
Future { promise in
center.getNotificationSettings { settings in
promise(.success(settings.authorizationStatus))
}
}.eraseToAnyPublisher()
},
getDeliveredNotifications: {
Future { callback in
center.getDeliveredNotifications { notifications in
callback(.success(notifications.map(Notification.init(rawValue:))))
}
}.eraseToAnyPublisher()
},
getNotificationSettings: {
Future { callback in
center.getNotificationSettings { settings in
callback(.success(NotificationSettings(rawValue: settings)))
}
}.eraseToAnyPublisher()
},
getNotificationCategories: {
Future { callback in
center.getNotificationCategories { categories in
callback(.success(categories))
}
}.eraseToAnyPublisher()
},
getPendingNotificationRequests: {
Future { callback in
center.getPendingNotificationRequests { requests in
callback(.success(requests.map(NotificationRequest.init(rawValue:))))
}
}.eraseToAnyPublisher()
},
removeAllDeliveredNotifications: {
center.removeAllDeliveredNotifications()
},
removeAllPendingNotificationRequests: {
center.removeAllPendingNotificationRequests()
},
removeDeliveredNotifications: {
center.removeDeliveredNotifications(withIdentifiers: $0)
},
removePendingNotificationRequests: {
center.removePendingNotificationRequests(withIdentifiers: $0)
},
requestAuthorization: { options in
Future { callback in
center.requestAuthorization(options: options) { (granted, error) in
if let error = error {
callback(.failure(error as NSError))
} else {
callback(.success(granted))
}
}
}.eraseToAnyPublisher()
},
setNotificationCategories: {
center.setNotificationCategories($0)
},
supportsContentExtensions: {
center.supportsContentExtensions
},
delegate: subject
.handleEvents(receiveCancel: { delegate = nil })
.eraseToAnyPublisher()
)
}
}
import Foundation
import Combine
extension UserNotificationClient {
static var mock: UserNotificationClient {
Self(
add: { _ in _unimplemented("add") },
getAuthStatus: { _unimplemented("getAuthStatus") },
getDeliveredNotifications: { _unimplemented("getDeliveredNotifications") },
getNotificationSettings: { _unimplemented("getNotificationSettings") },
getNotificationCategories: { _unimplemented("getNotificationCategories") },
getPendingNotificationRequests: { _unimplemented("getPendingNotificationRequests") },
removeAllDeliveredNotifications: { _unimplemented("removeAllDeliveredNotifications") },
removeAllPendingNotificationRequests: { _unimplemented("removeAllPendingNotificationRequests") },
removeDeliveredNotifications: { _ in _unimplemented("removeDeliveredNotifications") },
removePendingNotificationRequests: { _ in _unimplemented("removePendingNotificationRequests") },
requestAuthorization: { _ in _unimplemented("requestAuthorization") },
setNotificationCategories: { _ in _unimplemented("setNotificationCategories") },
supportsContentExtensions: { _unimplemented("supportsContentExtensions") },
delegate: Empty().eraseToAnyPublisher()
)
}
}
// see https://github.com/pointfreeco/swift-composable-architecture/blob/d39022f32b27725c5cdd24febc789f0933fa2329/Sources/ComposableCoreLocation/Mock.swift#L323
func _unimplemented(
_ function: StaticString, file: StaticString = #file, line: UInt = #line
) -> Never {
fatalError(
"""
`\(function)` was called but is not implemented. Be sure to provide an implementation for
this endpoint when creating the mock.
""",
file: file,
line: line
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment