diff --git a/DependencyGraph/pomonyang_dev_graph.png b/DependencyGraph/pomonyang_dev_graph.png index 051aa95..d726cc6 100644 Binary files a/DependencyGraph/pomonyang_dev_graph.png and b/DependencyGraph/pomonyang_dev_graph.png differ diff --git a/DependencyGraph/pomonyang_prod_graph.png b/DependencyGraph/pomonyang_prod_graph.png index 343ef29..129a48b 100644 Binary files a/DependencyGraph/pomonyang_prod_graph.png and b/DependencyGraph/pomonyang_prod_graph.png differ diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift index 14266f9..975d47c 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift @@ -11,4 +11,5 @@ import Foundation public enum Core: String, Modulable { case APIClient case KeychainClient + case UserNotificationClient } diff --git a/Projects/Core/APIClient/Project.swift b/Projects/Core/APIClient/Project.swift index 8825f2f..556e851 100644 --- a/Projects/Core/APIClient/Project.swift +++ b/Projects/Core/APIClient/Project.swift @@ -15,7 +15,6 @@ let project: Project = .makeTMABasedProject( .testing ], dependencies: [ - .sources: [], .interface: [ .dependency(rootModule: Shared.self) ] diff --git a/Projects/Core/KeychainClient/Project.swift b/Projects/Core/KeychainClient/Project.swift index 373d3ee..d95f83f 100644 --- a/Projects/Core/KeychainClient/Project.swift +++ b/Projects/Core/KeychainClient/Project.swift @@ -15,7 +15,6 @@ let project: Project = .makeTMABasedProject( .testing ], dependencies: [ - .sources: [], .interface: [ .dependency(rootModule: Shared.self) ] diff --git a/Projects/Core/UserNotificationClient/Interface/Interface.swift b/Projects/Core/UserNotificationClient/Interface/Interface.swift new file mode 100644 index 0000000..84c6017 --- /dev/null +++ b/Projects/Core/UserNotificationClient/Interface/Interface.swift @@ -0,0 +1,77 @@ +// +// Interface.swift +// UserNotificationClient +// +// Created by devMinseok on 7/20/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Combine +import UserNotifications + +import ComposableArchitecture + +@DependencyClient +public struct UserNotificationClient { + public var add: @Sendable (UNNotificationRequest) async throws -> Void + public var delegate: @Sendable () -> AsyncStream = { .finished } + public var getNotificationSettings: @Sendable () async -> Notification.Settings = { + Notification.Settings(authorizationStatus: .notDetermined) + } + public var removeDeliveredNotificationsWithIdentifiers: @Sendable ([String]) async -> Void + public var removePendingNotificationRequestsWithIdentifiers: @Sendable ([String]) async -> Void + public var requestAuthorization: @Sendable (UNAuthorizationOptions) async throws -> Bool + + @CasePathable + public enum DelegateEvent { + case didReceiveResponse(Notification.Response, completionHandler: @Sendable () -> Void) + case openSettingsForNotification(Notification?) + case willPresentNotification( + Notification, completionHandler: @Sendable (UNNotificationPresentationOptions) -> Void + ) + } + + public struct Notification: Equatable { + public var date: Date + public var request: UNNotificationRequest + + public init( + date: Date, + request: UNNotificationRequest + ) { + self.date = date + self.request = request + } + + public struct Response: Equatable { + public var notification: Notification + + public init(notification: Notification) { + self.notification = notification + } + } + + public struct Settings: Equatable { + public var authorizationStatus: UNAuthorizationStatus + + public init(authorizationStatus: UNAuthorizationStatus) { + self.authorizationStatus = authorizationStatus + } + } + } +} + + +// MARK: - DependencyValues + +extension DependencyValues { + public var userNotificationClient: UserNotificationClient { + get { self[UserNotificationClient.self] } + set { self[UserNotificationClient.self] = newValue } + } +} + +extension UserNotificationClient: TestDependencyKey { + public static let previewValue = Self() + public static let testValue = Self() +} diff --git a/Projects/Core/UserNotificationClient/Project.swift b/Projects/Core/UserNotificationClient/Project.swift new file mode 100644 index 0000000..8d67d9d --- /dev/null +++ b/Projects/Core/UserNotificationClient/Project.swift @@ -0,0 +1,20 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +@_spi(Core) +@_spi(Shared) +import DependencyPlugin + +let project: Project = .makeTMABasedProject( + module: Core.UserNotificationClient, + scripts: [], + targets: [ + .sources, + .interface + ], + dependencies: [ + .interface: [ + .dependency(rootModule: Shared.self) + ] + ] +) diff --git a/Projects/Core/UserNotificationClient/Sources/Implementation.swift b/Projects/Core/UserNotificationClient/Sources/Implementation.swift new file mode 100644 index 0000000..d47277f --- /dev/null +++ b/Projects/Core/UserNotificationClient/Sources/Implementation.swift @@ -0,0 +1,101 @@ +// +// Implementation.swift +// UserNotificationClient +// +// Created by devMinseok on 7/20/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Combine +import UserNotifications + +import UserNotificationClientInterface + +import Dependencies + +extension UserNotificationClient: DependencyKey { + public static let liveValue = Self( + add: { try await UNUserNotificationCenter.current().add($0) }, + delegate: { + AsyncStream { continuation in + let delegate = Delegate(continuation: continuation) + UNUserNotificationCenter.current().delegate = delegate + continuation.onTermination = { _ in + _ = delegate + } + } + }, + getNotificationSettings: { + await Notification.Settings( + rawValue: UNUserNotificationCenter.current().notificationSettings() + ) + }, + removeDeliveredNotificationsWithIdentifiers: { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: $0) + }, + removePendingNotificationRequestsWithIdentifiers: { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: $0) + }, + requestAuthorization: { + try await UNUserNotificationCenter.current().requestAuthorization(options: $0) + } + ) +} + +extension UserNotificationClient.Notification { + public init(rawValue: UNNotification) { + self.init(date: rawValue.date, request: rawValue.request) + } +} + +extension UserNotificationClient.Notification.Response { + public init(rawValue: UNNotificationResponse) { + self.init(notification: .init(rawValue: rawValue.notification)) + } +} + +extension UserNotificationClient.Notification.Settings { + public init(rawValue: UNNotificationSettings) { + self.init(authorizationStatus: rawValue.authorizationStatus) + } +} + +extension UserNotificationClient { + fileprivate class Delegate: NSObject, UNUserNotificationCenterDelegate { + let continuation: AsyncStream.Continuation + + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + self.continuation.yield( + .didReceiveResponse(.init(rawValue: response)) { completionHandler() } + ) + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + openSettingsFor notification: UNNotification? + ) { + self.continuation.yield( + .openSettingsForNotification(notification.map(Notification.init(rawValue:))) + ) + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: + @escaping (UNNotificationPresentationOptions) -> Void + ) { + self.continuation.yield( + .willPresentNotification(.init(rawValue: notification)) { completionHandler($0) } + ) + } + } +} diff --git a/Projects/Domain/AppService/Project.swift b/Projects/Domain/AppService/Project.swift index 7c0c537..92351a9 100644 --- a/Projects/Domain/AppService/Project.swift +++ b/Projects/Domain/AppService/Project.swift @@ -15,7 +15,6 @@ let project: Project = .makeTMABasedProject( .testing ], dependencies: [ - .sources: [], .interface: [ .dependency(rootModule: Core.self) ] diff --git a/Projects/Feature/AppFeature/Project.swift b/Projects/Feature/AppFeature/Project.swift index ac9e802..52b2aa5 100644 --- a/Projects/Feature/AppFeature/Project.swift +++ b/Projects/Feature/AppFeature/Project.swift @@ -15,7 +15,6 @@ let project: Project = .makeTMABasedProject( .testing ], dependencies: [ - .sources: [], .interface: [ .dependency(rootModule: Domain.self) ]