Last active
July 14, 2021 06:25
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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