From f5521da4c2ab81b57be1f07aeb68ea628c5ae14c Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sat, 5 Nov 2022 17:46:23 +0100 Subject: [PATCH 01/11] Updates to reducer protocol and dependency keys. --- .../xcshareddata/swiftpm/Package.resolved | 57 +++- .../Example/Example.xcodeproj/project.pbxproj | 8 +- Examples/Example/Example/App.swift | 128 ++++++++ Examples/Example/Example/AppState.swift | 118 ------- Examples/Example/Example/ContentView.swift | 12 +- Examples/Example/Example/RemoteClient.swift | 27 +- Examples/Example/Example/SceneDelegate.swift | 16 +- .../Example/ExampleTests/ExampleTests.swift | 290 +++++++++--------- Package.swift | 2 +- README.md | 87 +++--- .../Interface.swift | 73 ++--- .../ComposableUserNotifications/Live.swift | 166 ---------- .../ComposableUserNotifications/LiveKey.swift | 126 ++++++++ .../ComposableUserNotifications/Mock.swift | 119 ------- .../ComposableUserNotifications/TestKey.swift | 77 +++++ 15 files changed, 639 insertions(+), 667 deletions(-) create mode 100644 Examples/Example/Example/App.swift delete mode 100644 Examples/Example/Example/AppState.swift delete mode 100644 Sources/ComposableUserNotifications/Live.swift create mode 100644 Sources/ComposableUserNotifications/LiveKey.swift delete mode 100644 Sources/ComposableUserNotifications/Mock.swift create mode 100644 Sources/ComposableUserNotifications/TestKey.swift diff --git a/ComposableUserNotifications.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableUserNotifications.xcworkspace/xcshareddata/swiftpm/Package.resolved index 95911b1..88c1591 100644 --- a/ComposableUserNotifications.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ComposableUserNotifications.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", "state": { "branch": null, - "revision": "ff42ec9061d864de7982162011321d3df5080c10", - "version": "0.1.2" + "revision": "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", + "version": "0.9.1" } }, { @@ -15,8 +15,26 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "ed1838ab4fa5d47db8aa640736dd5b7672548873", - "version": "0.1.2" + "revision": "bb436421f57269fbcfe7360735985321585a86e5", + "version": "0.10.1" + } + }, + { + "package": "swift-clocks", + "repositoryURL": "https://github.com/pointfreeco/swift-clocks", + "state": { + "branch": null, + "revision": "692ec4f5429a667bdd968c7260dfa2b23adfeffc", + "version": "0.1.4" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" } }, { @@ -24,8 +42,35 @@ "repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture", "state": { "branch": null, - "revision": "b67569f69813140cd9c984db33ee959d9711a008", - "version": "0.9.0" + "revision": "1fcd53fc875bade47d850749ea53c324f74fd64d", + "version": "0.45.0" + } + }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "819d9d370cd721c9d87671e29d947279292e4541", + "version": "0.6.0" + } + }, + { + "package": "swift-identified-collections", + "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", + "state": { + "branch": null, + "revision": "bfb0d43e75a15b6dfac770bf33479e8393884a36", + "version": "0.4.1" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784", + "version": "0.5.0" } } ] diff --git a/Examples/Example/Example.xcodeproj/project.pbxproj b/Examples/Example/Example.xcodeproj/project.pbxproj index 3a768da..da6199a 100644 --- a/Examples/Example/Example.xcodeproj/project.pbxproj +++ b/Examples/Example/Example.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ 4AA682B2254D824B00031ADC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AA682B1254D824B00031ADC /* Assets.xcassets */; }; 4AA682C0254D824B00031ADC /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA682BF254D824B00031ADC /* ExampleTests.swift */; }; 4AA682E0254D827C00031ADC /* ComposableUserNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 4AA682DF254D827C00031ADC /* ComposableUserNotifications */; }; - 4AA682E7254D870C00031ADC /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA682E6254D870C00031ADC /* AppState.swift */; }; + 4AA682E7254D870C00031ADC /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA682E6254D870C00031ADC /* App.swift */; }; 4AFBD623254EC40900977967 /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBD622254EC40900977967 /* Helper.swift */; }; /* End PBXBuildFile section */ @@ -41,7 +41,7 @@ 4AA682BB254D824B00031ADC /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4AA682BF254D824B00031ADC /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; 4AA682C1254D824B00031ADC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4AA682E6254D870C00031ADC /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + 4AA682E6254D870C00031ADC /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 4AFBD622254EC40900977967 /* Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helper.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -86,7 +86,7 @@ 4AA682AC254D824900031ADC /* Example */ = { isa = PBXGroup; children = ( - 4AA682E6254D870C00031ADC /* AppState.swift */, + 4AA682E6254D870C00031ADC /* App.swift */, 4A19F314254D8B9300E429F0 /* BackgroundNotification.swift */, 4AA682AF254D824900031ADC /* ContentView.swift */, 4AFBD622254EC40900977967 /* Helper.swift */, @@ -216,7 +216,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4AA682E7254D870C00031ADC /* AppState.swift in Sources */, + 4AA682E7254D870C00031ADC /* App.swift in Sources */, 4AFBD623254EC40900977967 /* Helper.swift in Sources */, 4AA682B0254D824900031ADC /* ContentView.swift in Sources */, 4A19F319254D8BB700E429F0 /* UserNotification.swift in Sources */, diff --git a/Examples/Example/Example/App.swift b/Examples/Example/Example/App.swift new file mode 100644 index 0000000..e71c60a --- /dev/null +++ b/Examples/Example/Example/App.swift @@ -0,0 +1,128 @@ +// +// AppState.swift +// Example +// +// Created by Michael Kao on 31.10.20. +// + +import Foundation +import ComposableArchitecture +import ComposableUserNotifications +import UIKit + +struct App: ReducerProtocol { + struct State: Equatable { + var count: Int? + } + + enum Action: Equatable { + case addNotificationResponse(TaskResult) + case didFinishLaunching(notification: UserNotification?) + case didReceiveBackgroundNotification(BackgroundNotification) + case remoteCountResponse(TaskResult) + case requestAuthorizationResponse(TaskResult) + case tappedScheduleButton + case userNotifications(UserNotificationClient.DeletegateAction) + } + + @Dependency(\.remote) var remote + @Dependency(\.userNotifications) var userNotifications + + func reduce(into state: inout State, action: Action) -> EffectTask { + switch action { + case let .didFinishLaunching(notification): + if case let .count(value) = notification { + state.count = value + } + + return .run { send in + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for await event in self.userNotifications.delegate() { + await send(.userNotifications(event)) + } + } + + group.addTask { + await send( + .requestAuthorizationResponse( + TaskResult { + try await self.userNotifications.requestAuthorization([.alert, .badge, .sound]) + } + ) + ) + } + } + } + + case let .didReceiveBackgroundNotification(backgroundNotification): + let fetchCompletionHandler = backgroundNotification.fetchCompletionHandler + guard backgroundNotification.content == .countAvailable else { + return .fireAndForget { + backgroundNotification.fetchCompletionHandler(.noData) + } + } + + return .task { + do { + let count = try await self.remote.fetchRemoteCount() + fetchCompletionHandler(.newData) + return .remoteCountResponse(.success(count)) + } catch { + fetchCompletionHandler(.failed) + return .remoteCountResponse(.failure(error)) + } + } + + case let .remoteCountResponse(.success(count)): + state.count = count + return .none + + case .remoteCountResponse(.failure): + return .none + + case let .userNotifications(.willPresentNotification(_, completion)): + return .fireAndForget { + completion([.list, .banner, .sound]) + } + + case let .userNotifications(.didReceiveResponse(response, completion)): + let notification = UserNotification(userInfo: response.notification.request.content.userInfo()) + if case let .count(value) = notification { + state.count = value + } + + return .fireAndForget(completion) + + case .userNotifications(.openSettingsForNotification): + return .none + + case .requestAuthorizationResponse: + return .none + + case .addNotificationResponse: + return .none + + case .tappedScheduleButton: + let content = UNMutableNotificationContent() + content.title = "Example title" + content.body = "Example body" + + let request = UNNotificationRequest( + identifier: "example_notification", + content: content, + trigger: UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) + ) + + return .task { + await self.userNotifications + .removePendingNotificationRequestsWithIdentifiers(["example_notification"]) + return await .addNotificationResponse( + TaskResult { + Unit(try await self.userNotifications.add(request)) + } + ) + } + } + } +} diff --git a/Examples/Example/Example/AppState.swift b/Examples/Example/Example/AppState.swift deleted file mode 100644 index 86372db..0000000 --- a/Examples/Example/Example/AppState.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// AppState.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - -import Foundation -import ComposableArchitecture -import ComposableUserNotifications -import UIKit - -struct AppState: Equatable { - var count: Int? -} - -enum AppAction: Equatable { - case addNotificationResponse(Result) - case didFinishLaunching(notification: UserNotification?) - case didReceiveBackgroundNotification(BackgroundNotification) - case remoteCountResponse(Result) - case requestAuthorizationResponse(Result) - case tappedScheduleButton - case userNotification(UserNotificationClient.Action) -} - -struct AppEnvironment { - var remoteClient: RemoteClient - var userNotificationClient: UserNotificationClient -} - -let appReducer = Reducer { state, action, environment in - switch action { - case let .didFinishLaunching(notification): - if case let .count(value) = notification { - state.count = value - } - - return .merge( - environment.userNotificationClient - .delegate() - .map(AppAction.userNotification), - environment.userNotificationClient.requestAuthorization([.alert, .badge, .sound]) - .catchToEffect() - .map(AppAction.requestAuthorizationResponse) - ) - - case let .didReceiveBackgroundNotification(backgroundNotification): - let fetchCompletionHandler = backgroundNotification.fetchCompletionHandler - guard backgroundNotification.content == .countAvailable else { - return .fireAndForget { - backgroundNotification.fetchCompletionHandler(.noData) - } - } - - return environment.remoteClient.fetchRemoteCount() - .catchToEffect() - .handleEvents(receiveOutput: { result in - switch result { - case .success: - fetchCompletionHandler(.newData) - case .failure: - fetchCompletionHandler(.failed) - } - }) - .eraseToEffect() - .map(AppAction.remoteCountResponse) - - case let .remoteCountResponse(.success(count)): - state.count = count - return .none - - case .remoteCountResponse(.failure): - return .none - - case let .userNotification(.willPresentNotification(notification, completion)): - return .fireAndForget { - completion([.list, .banner, .sound]) - } - - case let .userNotification(.didReceiveResponse(response, completion)): - let notification = UserNotification(userInfo: response.notification.request.content.userInfo()) - if case let .count(value) = notification { - state.count = value - } - - return .fireAndForget(completion) - - case .userNotification(.openSettingsForNotification): - return .none - - case .requestAuthorizationResponse: - return .none - - case .addNotificationResponse: - return .none - - case .tappedScheduleButton: - let content = UNMutableNotificationContent() - content.title = "Example title" - content.body = "Example body" - - let request = UNNotificationRequest( - identifier: "example_notification", - content: content, - trigger: UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) - ) - - return .concatenate( - environment.userNotificationClient.removePendingNotificationRequestsWithIdentifiers(["example_notification"]) - .fireAndForget(), - environment.userNotificationClient.add(request) - .map(Unit.init) - .catchToEffect() - .map(AppAction.addNotificationResponse) - ) - } -} diff --git a/Examples/Example/Example/ContentView.swift b/Examples/Example/Example/ContentView.swift index 28e1171..e3b3fa0 100644 --- a/Examples/Example/Example/ContentView.swift +++ b/Examples/Example/Example/ContentView.swift @@ -10,7 +10,7 @@ import ComposableUserNotifications import SwiftUI struct ContentView: View { - let store: Store + let store: StoreOf var body: some View { WithViewStore(self.store) { viewStore in @@ -30,12 +30,10 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView( store: Store( - initialState: AppState(), - reducer: .empty, - environment: AppEnvironment( - remoteClient: RemoteClient(fetchRemoteCount: { Effect(value: 1) }), - userNotificationClient: .mock() - ) + initialState: .init(), + reducer: App() + .dependency(\.userNotifications, .previewValue) + .dependency(\.remote, .previewValue) ) ) } diff --git a/Examples/Example/Example/RemoteClient.swift b/Examples/Example/Example/RemoteClient.swift index 4fff45c..8e9cb52 100644 --- a/Examples/Example/Example/RemoteClient.swift +++ b/Examples/Example/Example/RemoteClient.swift @@ -7,11 +7,32 @@ import Foundation import ComposableArchitecture +import XCTestDynamicOverlay struct RemoteClient { - var fetchRemoteCount: () -> Effect + var fetchRemoteCount: () async throws -> Int +} - public struct Error: Swift.Error, Equatable { - public init() {} +extension DependencyValues { + var remote: RemoteClient { + get { self[RemoteClient.self] } + set { self[RemoteClient.self] = newValue } } } + +extension RemoteClient: DependencyKey { + static let liveValue = Self( + fetchRemoteCount: { 1 } + ) +} + +extension RemoteClient: TestDependencyKey { + static let previewValue = Self( + fetchRemoteCount: { 666 } + ) + + static let testValue = Self( + fetchRemoteCount: XCTUnimplemented("\(Self.self).fetchRemoteCount") + ) +} + diff --git a/Examples/Example/Example/SceneDelegate.swift b/Examples/Example/Example/SceneDelegate.swift index a9b2e12..48d642f 100644 --- a/Examples/Example/Example/SceneDelegate.swift +++ b/Examples/Example/Example/SceneDelegate.swift @@ -11,12 +11,11 @@ import UIKit import Combine private let store = Store( - initialState: AppState(), - reducer: appReducer, - environment: AppEnvironment( - remoteClient: .randomDelayed, - userNotificationClient: .live - ) + initialState: App.State(), + reducer: App().transformDependency(\.self) { + $0.remote = .liveValue + $0.userNotifications = .liveValue + } ) class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -62,9 +61,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension RemoteClient { static let randomDelayed = RemoteClient( fetchRemoteCount: { - Effect(value: Int.random(in: 0...10)) - .delay(for: 2, scheduler: DispatchQueue.main) - .eraseToEffect() + try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2) + return Int.random(in: 0...10) } ) } diff --git a/Examples/Example/ExampleTests/ExampleTests.swift b/Examples/Example/ExampleTests/ExampleTests.swift index 72ef093..8784eaf 100644 --- a/Examples/Example/ExampleTests/ExampleTests.swift +++ b/Examples/Example/ExampleTests/ExampleTests.swift @@ -12,66 +12,67 @@ import struct ComposableUserNotifications.Notification import XCTest @testable import Example +@MainActor class ExampleTests: XCTestCase { - var environment = AppEnvironment( - remoteClient: RemoteClient(fetchRemoteCount: { Effect(value: 5) }), - userNotificationClient: .mock( - requestAuthorization: { _ in Effect(value: true) } - ) - ) - - func testApplicationLaunchWithoutNotification() throws { - let delegateActionSubject = PassthroughSubject() - var didSubscribeNotifications = false - var didRequestAuthrizationOptions: UNAuthorizationOptions? - environment.userNotificationClient.requestAuthorization = { options in - didRequestAuthrizationOptions = options - return Effect(value: true) + func testApplicationLaunchWithoutNotification() async throws { + let delegate = AsyncStream.streamWithContinuation() + let requestedAuthorizationOptions = ActorIsolated(nil) + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) + store.dependencies.userNotifications.delegate = { delegate.stream } + store.dependencies.userNotifications.requestAuthorization = { options in + await requestedAuthorizationOptions.setValue(options) + return true } - - environment.userNotificationClient.delegate = { - didSubscribeNotifications = true - return delegateActionSubject.eraseToEffect() + let task = await store.send(.didFinishLaunching(notification: nil)) + await store.receive(.requestAuthorizationResponse(.success(true))) + await requestedAuthorizationOptions.withValue { + XCTAssertNoDifference($0, [.alert, .badge, .sound]) } - - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didFinishLaunching(notification: nil)), - .receive(.requestAuthorizationResponse(.success(true))), - .do { XCTAssertEqual(didRequestAuthrizationOptions, [.alert, .badge, .sound]) }, - .do { XCTAssertTrue(didSubscribeNotifications) }, - .do { delegateActionSubject.send(completion: .finished) } - ) + await task.cancel() } - func testApplicationLaunchWithtNotification() throws { - let delegateActionSubject = PassthroughSubject() - environment.userNotificationClient.delegate = { delegateActionSubject.eraseToEffect() } - - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didFinishLaunching(notification: .count(5))) { - $0.count = 5 - }, - .receive(.requestAuthorizationResponse(.success(true))), - .do { delegateActionSubject.send(completion: .finished) } + func testApplicationLaunchWithtNotification() async throws { + let delegate = AsyncStream.streamWithContinuation() + let requestedAuthorizationOptions = ActorIsolated(nil) + + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() ) + store.dependencies.userNotifications.delegate = { delegate.stream } + store.dependencies.userNotifications.requestAuthorization = { options in + await requestedAuthorizationOptions.setValue(options) + return true + } + + let task = await store.send(.didFinishLaunching(notification: .count(5))) { + $0.count = 5 + } + await store.receive(.requestAuthorizationResponse(.success(true))) + await requestedAuthorizationOptions.withValue { + XCTAssertNoDifference($0, [.alert, .badge, .sound]) + } + await task.cancel() } + func testNotificationPresentationHandling() async throws { + let delegate = AsyncStream.streamWithContinuation() - func testNotificationPresentationHandling() throws { - let delegateActionSubject = PassthroughSubject() - environment.userNotificationClient.delegate = { delegateActionSubject.eraseToEffect() } + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) + store.dependencies.userNotifications.requestAuthorization = { _ in true } + store.dependencies.userNotifications.delegate = { delegate.stream } - var presentationOptions: UNNotificationPresentationOptions? - let completion = { presentationOptions = $0 } + let task = await store.send(.didFinishLaunching(notification: nil)) + await store.receive(.requestAuthorizationResponse(.success(true))) + var notificationPresentationOptions: UNNotificationPresentationOptions? + let willPresentNotificationCompletionHandler = { notificationPresentationOptions = $0 } let content = UNMutableNotificationContent() content.userInfo = ["count": 5] let notification = Notification( @@ -83,27 +84,39 @@ class ExampleTests: XCTestCase { ) ) - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didFinishLaunching(notification: nil)), - .receive(.requestAuthorizationResponse(.success(true))), - .do { delegateActionSubject.send(.willPresentNotification(notification, completion: completion)) }, - .receive(.userNotification(.willPresentNotification(notification, completion: completion))), - .do { XCTAssertEqual(presentationOptions, [.list, .banner, .sound]) }, - .do { delegateActionSubject.send(completion: .finished) } + delegate.continuation.yield( + .willPresentNotification( + notification, + completionHandler: { willPresentNotificationCompletionHandler($0) } + ) + ) + await store.receive( + .userNotifications( + .willPresentNotification( + notification, + completionHandler: willPresentNotificationCompletionHandler + ) + ) ) + XCTAssertNoDifference(notificationPresentationOptions, [.list, .banner, .sound]) + await task.cancel() } - func testReceivedNotification() throws { - let delegateActionSubject = PassthroughSubject() - environment.userNotificationClient.delegate = { delegateActionSubject.eraseToEffect() } + func testReceivedNotification() async throws { + let delegate = AsyncStream.streamWithContinuation() + + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) + store.dependencies.userNotifications.requestAuthorization = { _ in true } + store.dependencies.userNotifications.delegate = { delegate.stream } - var didComplete = false - let completion = { didComplete = true } + let task = await store.send(.didFinishLaunching(notification: nil)) + await store.receive(.requestAuthorizationResponse(.success(true))) + var didReceiveResponseCompletionHandlerCalled = false + let didReceiveResponseCompletionHandler = { didReceiveResponseCompletionHandlerCalled = true } let content = UNMutableNotificationContent() content.userInfo = ["count": 5] let response = Notification.Response.user( @@ -120,25 +133,29 @@ class ExampleTests: XCTestCase { ) ) - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didFinishLaunching(notification: nil)), - .receive(.requestAuthorizationResponse(.success(true))), - .do { delegateActionSubject.send(.didReceiveResponse(response, completion: completion)) }, - .receive(.userNotification(.didReceiveResponse(response, completion: completion))) { - $0.count = 5 - }, - .do { XCTAssertTrue(didComplete) }, - .do { delegateActionSubject.send(completion: .finished) } + delegate.continuation.yield( + .didReceiveResponse(response, completionHandler: { didReceiveResponseCompletionHandler() }) ) + await store.receive( + .userNotifications( + .didReceiveResponse( + response, + completionHandler: didReceiveResponseCompletionHandler + ) + ) + ) { + $0.count = 5 + } + XCTAssert(didReceiveResponseCompletionHandlerCalled) + await task.cancel() } - func testReceiveBackgroundNotification() throws { - let delegateActionSubject = PassthroughSubject() - environment.userNotificationClient.delegate = { delegateActionSubject.eraseToEffect() } + func testReceiveBackgroundNotification() async throws { + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) + store.dependencies.remote.fetchRemoteCount = { 5 } var backgroundFetchResult: UIBackgroundFetchResult? let backgroundNotification = BackgroundNotification( @@ -147,24 +164,20 @@ class ExampleTests: XCTestCase { fetchCompletionHandler: { backgroundFetchResult = $0 } ) - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didReceiveBackgroundNotification(backgroundNotification)), - .receive(.remoteCountResponse(.success(5))) { - $0.count = 5 - }, - .do { XCTAssertEqual(backgroundFetchResult, .newData) }, - .do { delegateActionSubject.send(completion: .finished) } - ) + await store.send(.didReceiveBackgroundNotification(backgroundNotification)) + await store.receive(.remoteCountResponse(.success(5))) { + $0.count = 5 + } + XCTAssertNoDifference(backgroundFetchResult, .newData) } - func testReceiveBackgroundNotificationFailure() throws { - let delegateActionSubject = PassthroughSubject() - environment.userNotificationClient.delegate = { delegateActionSubject.eraseToEffect() } - environment.remoteClient.fetchRemoteCount = { Effect(error: RemoteClient.Error()) } + func testReceiveBackgroundNotificationFailure() async throws { + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) + struct Error: Swift.Error, Equatable {} + store.dependencies.remote.fetchRemoteCount = { throw Error() } var backgroundFetchResult: UIBackgroundFetchResult? let backgroundNotification = BackgroundNotification( @@ -173,21 +186,16 @@ class ExampleTests: XCTestCase { fetchCompletionHandler: { backgroundFetchResult = $0 } ) - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didReceiveBackgroundNotification(backgroundNotification)), - .receive(.remoteCountResponse(.failure(RemoteClient.Error()))), - .do { XCTAssertEqual(backgroundFetchResult, .failed) }, - .do { delegateActionSubject.send(completion: .finished) } - ) + await store.send(.didReceiveBackgroundNotification(backgroundNotification)) + await store.receive(.remoteCountResponse(.failure(Error()))) + XCTAssertNoDifference(backgroundFetchResult, .failed) } - func testReceiveBackgroundNotificationWithoutContent() throws { - let delegateActionSubject = PassthroughSubject() - environment.userNotificationClient.delegate = { delegateActionSubject.eraseToEffect() } + func testReceiveBackgroundNotificationWithoutContent() async throws { + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) var backgroundFetchResult: UIBackgroundFetchResult? let backgroundNotification = BackgroundNotification( @@ -196,43 +204,39 @@ class ExampleTests: XCTestCase { fetchCompletionHandler: { backgroundFetchResult = $0 } ) - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.didReceiveBackgroundNotification(backgroundNotification)), - .do { XCTAssertEqual(backgroundFetchResult, .noData) }, - .do { delegateActionSubject.send(completion: .finished) } - ) + await store.send(.didReceiveBackgroundNotification(backgroundNotification)) + XCTAssertNoDifference(backgroundFetchResult, .noData) } - func testTappedScheduleButton() throws { - var notificationRequest: UNNotificationRequest? - environment.userNotificationClient.add = { request in - notificationRequest = request - return Effect(value: ()) + func testTappedScheduleButton() async throws { + let store = TestStore( + initialState: App.State(count: nil), + reducer: App() + ) + + let notificationRequest = ActorIsolated(nil) + let removedPendingIdentifiers = ActorIsolated<[String]?>(nil) + + store.dependencies.userNotifications.add = { request in + await notificationRequest.setValue(request) } - var removedPendingIdentifiers: [String]? - environment.userNotificationClient.removePendingNotificationRequestsWithIdentifiers = { identifiers in - removedPendingIdentifiers = identifiers - return .fireAndForget {} + store.dependencies.userNotifications.removePendingNotificationRequestsWithIdentifiers = { identifiers in + await removedPendingIdentifiers.setValue(identifiers) } - TestStore( - initialState: AppState(count: nil), - reducer: appReducer, - environment: environment - ).assert( - .send(.tappedScheduleButton), - .receive(.addNotificationResponse(.success(Unit()))), - .do { XCTAssertEqual(removedPendingIdentifiers, ["example_notification"]) }, - .do { XCTAssertEqual(notificationRequest?.content.title, "Example title") }, - .do { XCTAssertEqual(notificationRequest?.content.body, "Example body") }, - .do { XCTAssertTrue(notificationRequest?.trigger is UNTimeIntervalNotificationTrigger) }, - .do { XCTAssertEqual( - (notificationRequest?.trigger as? UNTimeIntervalNotificationTrigger)?.timeInterval, 5 - )} - ) + await store.send(.tappedScheduleButton) + await store.receive(.addNotificationResponse(.success(Unit()))) + await removedPendingIdentifiers.withValue { + XCTAssertNoDifference($0, ["example_notification"]) + } + await notificationRequest.withValue { + XCTAssertEqual($0?.content.title, "Example title") + XCTAssertEqual($0?.content.body, "Example body") + XCTAssertTrue($0?.trigger is UNTimeIntervalNotificationTrigger) + XCTAssertEqual( + ($0?.trigger as? UNTimeIntervalNotificationTrigger)?.timeInterval, + 5 + ) + } } } diff --git a/Package.swift b/Package.swift index dfc956f..407a87f 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.8.0"), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.45.0"), ], targets: [ .target( diff --git a/README.md b/README.md index 38eb148..2e51fd4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Composable User Notifications is library that bridges [the Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) and [User Notifications](https://developer.apple.com/documentation/usernotifications). +This library is using the [ReducerProtocol](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/reducerprotocol/) and models it's dependency using [swift concurrency](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/swiftconcurrency) since version 0.3.0. + * [Example](#example) * [Basic usage](#basic-usage) * [Installation](#installation) @@ -10,46 +12,46 @@ Composable User Notifications is library that bridges [the Composable Architectu Check out the Example demo to see how [ComposableUserNotifications](./Examples/Example) can be used. ## Basic usage -To handle incoming user notification you can observe the `UNUserNotificationCenterDelegate` actions `UserNotificationClient.Action` of the `UserNotificationClient.delegate` effect. +To handle incoming user notification you can observe the `UNUserNotificationCenterDelegate` actions through `UserNotificationClient.DelegateAction` of the `UserNotificationClient.delegate`. ```swift import ComposableUserNotifications -enum AppAction { - case userNotification(UserNotificationClient.Action) - +struct App: ReducerProtocol { + enum Action { + case userNotification(UserNotificationClient.DelegateAction) // Your domain's other actions: - ... -} +... ``` -The `UserNotificationClient.Action` holds the actions + +The `UserNotificationClient.DelegateAction` holds the actions * for handling foreground notifications `willPresentNotification(_:completion)` -* to process the user's response to a delivered notification `didReceiveResponse(_:completion:)` +* to process the user's response to a delivered notification `didReceiveResponse(_:completionHandler:)` * to display the in-app notification settings `openSettingsForNotification(_:)` -The wrapper around apple's `UNUserNotificationCenter` `UserNotificationClient`, should be part of your applications environment. -```swift -struct AppEnvironment { - var userNotificationClient: UserNotificationClient - - // Your domain's other dependencies: - ... -} -``` +The wrapper around apple's `UNUserNotificationCenter` `UserNotificationClient`, is available on the `DependencyValues` and can be retrieved on using `@Dependency(\.userNotifications)`. -At some point you need to subscribe to `UserNotificationClient.Action` in order not to miss any `UNUserNotificationCenterDelegate` related actions. This can be done early after starting the application. +At some point you need to subscribe to `UserNotificationClient.DelegateAction` in order not to miss any `UNUserNotificationCenterDelegate` related actions. This can be done early after starting the application. ```swift -let appReducer = Reducer { state, action, environment in +func reduce(into state: inout State, action: Action) -> EffectTask { switch action { - case .didFinishLaunching: // or onAppear of your first View - return environment.userNotificationClient - .delegate() - .map(AppAction.userNotification) + case let .didFinishLaunching(notification): + ... + return .run { send in + for await event in self.userNotifications.delegate() { + await send(.userNotifications(event)) + } + } + } + } +} ``` + When subscribing to these actions we can handle them as follows. ```swift +... case let .userNotification(.willPresentNotification(notification, completion)): return .fireAndForget { completion([.list, .banner, .sound]) @@ -62,20 +64,25 @@ When subscribing to these actions we can handle them as follows. case .userNotification(.openSettingsForNotification): return .none +... ``` To request authorization from the user you can use `requestAuthorization` and handle the users choice as a new action. ```swift -let appReducer = Reducer { state, action, environment in +func reduce(into state: inout State, action: Action) -> EffectTask { switch action { case .didFinishLaunching: - return .merge( - ..., - environment.userNotificationClient.requestAuthorization([.alert, .badge, .sound]) - .catchToEffect() - .map(AppAction.requestAuthorizationResponse) + return .task { + .requestAuthorizationResponse( + TaskResult { + try await self.userNotifications.requestAuthorization([.alert, .badge, .sound]) + } ) + } + } + ... +} ``` Adding notification requests is also straight forward. It can be done using `UNNotificationRequest` in conjunction with `UserNotificationClient.add(_:)`. @@ -92,17 +99,19 @@ Adding notification requests is also straight forward. It can be done using `UNN trigger: UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) ) - return .concatenate( - environment.userNotificationClient.removePendingNotificationRequestsWithIdentifiers(["example_notification"]) - .fireAndForget(), - environment.userNotificationClient.add(request) - .map(Unit.init) - .catchToEffect() - .map(AppAction.addNotificationResponse) - ) + return .task { + await self.userNotifications + .removePendingNotificationRequestsWithIdentifiers(["example_notification"]) + return await .addNotificationResponse( + TaskResult { + Unit(try await self.userNotifications.add(request)) + } + ) + } + ... ``` -There are of course a lot more wrapped API calls to `UNUserNotificationCenter` available. -The true power of this approach again lies in the testability of your notification logic. +All API calls to `UNUserNotificationCenter` are available through `UserNotificationClient`. +The true power of this approach lies in the testability of your notification logic. For more info around testability have a look at [ExampleTests.swift](./Examples/Example/ExampleTests/ExampleTests.swift). ## Installation diff --git a/Sources/ComposableUserNotifications/Interface.swift b/Sources/ComposableUserNotifications/Interface.swift index 5200ea1..fc8c731 100644 --- a/Sources/ComposableUserNotifications/Interface.swift +++ b/Sources/ComposableUserNotifications/Interface.swift @@ -1,6 +1,7 @@ import CoreLocation import ComposableArchitecture import UserNotifications +import XCTestDynamicOverlay /// A wrapper around UserNotifications's `UNUserNotificationCenter` that exposes its functionality through /// effects and actions, making it easy to use with the Composable Architecture and easy to test. @@ -13,88 +14,56 @@ public struct UserNotificationClient { /// Actions that correspond to `UNUserNotificationCenterDelegate` methods. /// /// See `UNUserNotificationCenterDelegate` for more information. - public enum Action { + public enum DeletegateAction { case willPresentNotification( _ notification: Notification, - completion: (UNNotificationPresentationOptions) -> Void) + completionHandler: (UNNotificationPresentationOptions) -> Void) @available(tvOS, unavailable) - case didReceiveResponse(_ response: Notification.Response, completion: () -> Void) + case didReceiveResponse(_ response: Notification.Response, completionHandler: () -> Void) case openSettingsForNotification(_ notification: Notification?) } - public var add: (UNNotificationRequest) -> Effect = { _ in - _unimplemented("add") - } + public var add: @Sendable (UNNotificationRequest) async throws -> Void = + XCTUnimplemented("\(Self.self).add") @available(tvOS, unavailable) - public var getDeliveredNotifications: () -> Effect<[Notification], Never> = { - _unimplemented("getDeliveredNotifications") - } + public var deliveredNotifications: @Sendable () async -> [Notification] = XCTUnimplemented("\(Self.self).deliveredNotifications") @available(tvOS, unavailable) - public var getNotificationCategories: () -> Effect, Never> = { - _unimplemented("getNotificationCategories") - } + public var notificationCategories: () async -> Set = XCTUnimplemented("\(Self.self).deliveredNotifications") - public var getNotificationSettings: () -> Effect = { - _unimplemented("getNotificationSettings") - } + public var notificationSettings: () async -> Notification.Settings = XCTUnimplemented("\(Self.self).notificationSettings") - public var getPendingNotificationRequests: () -> Effect<[Notification.Request], Never> = { - _unimplemented("getPendingNotificationRequests") - } + public var pendingNotificationRequests: () async -> [Notification.Request] = XCTUnimplemented("\(Self.self).pendingNotificationRequests") @available(tvOS, unavailable) - public var removeAllDeliveredNotifications: () -> Effect = { - _unimplemented("removeAllDeliveredNotifications") - } + public var removeAllDeliveredNotifications: () async -> Void = XCTUnimplemented("\(Self.self).removeAllDeliveredNotifications") - public var removeAllPendingNotificationRequests: () -> Effect = { - _unimplemented("removeAllPendingNotificationRequests") - } + public var removeAllPendingNotificationRequests: () async -> Void = XCTUnimplemented("\(Self.self).removeAllPendingNotificationRequests") @available(tvOS, unavailable) - public var removeDeliveredNotificationsWithIdentifiers: ([String]) -> Effect = { _ in - _unimplemented("removeDeliveredNotificationsWithIdentifiers") - } + public var removeDeliveredNotificationsWithIdentifiers: ([String]) async -> Void = XCTUnimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers") - public var removePendingNotificationRequestsWithIdentifiers: ([String]) -> Effect = { _ in - _unimplemented("removePendingNotificationRequestsWithIdentifiers") - } + public var removePendingNotificationRequestsWithIdentifiers: ([String]) async -> Void = XCTUnimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers") - public var requestAuthorization: (UNAuthorizationOptions) -> Effect = { _ in - _unimplemented("requestAuthorization") - } + public var requestAuthorization: (UNAuthorizationOptions) async throws -> Bool = + XCTUnimplemented("\(Self.self).requestAuthorization") @available(tvOS, unavailable) - public var setNotificationCategories: (Set) -> Effect = { _ in - _unimplemented("setNotificationCategories") - } + public var setNotificationCategories: (Set) async -> Void = XCTUnimplemented("\(Self.self).setNotificationCategories") - public var supportsContentExtensions: () -> Bool = { - _unimplemented("supportsContentExtensions") - } + public var supportsContentExtensions: () -> Bool = XCTUnimplemented("\(Self.self).supportsContentExtensions") /// This Effect represents calls to the `UNUserNotificationCenterDelegate`. /// Handling the completion handlers of the `UNUserNotificationCenterDelegate`s methods /// by multiple observers might lead to unexpected behaviour. - public var delegate: () -> Effect = { - _unimplemented("delegate") - } - - public struct Error: Swift.Error, Equatable { - public let error: NSError - - public init(_ error: Swift.Error) { - self.error = error as NSError - } - } + public var delegate: @Sendable () -> AsyncStream = XCTUnimplemented("\(Self.self).delegate", placeholder: .finished) } -extension UserNotificationClient.Action: Equatable { - public static func == (lhs: UserNotificationClient.Action, rhs: UserNotificationClient.Action) -> Bool { +extension UserNotificationClient.DeletegateAction: Equatable { + public static func == (lhs: UserNotificationClient.DeletegateAction, rhs: UserNotificationClient.DeletegateAction) -> Bool { switch (lhs, rhs) { case let (.willPresentNotification(lhs, _), .willPresentNotification(rhs, _)): return lhs == rhs diff --git a/Sources/ComposableUserNotifications/Live.swift b/Sources/ComposableUserNotifications/Live.swift deleted file mode 100644 index ba1d7eb..0000000 --- a/Sources/ComposableUserNotifications/Live.swift +++ /dev/null @@ -1,166 +0,0 @@ -import Foundation -import Combine -import ComposableArchitecture -import UserNotifications - -extension UserNotificationClient { - public static var live: UserNotificationClient { - let center = UNUserNotificationCenter.current() - - var client = UserNotificationClient() - client.add = { request in - .future { callback in - center.add(request) { error in - if let error = error { - callback(.failure(Error(error))) - } else { - callback(.success(())) - } - } - } - } - - #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.getDeliveredNotifications = { - .future { callback in - center.getDeliveredNotifications { notifications in - callback(.success(notifications.map(Notification.init(rawValue:)))) - } - } - } - #endif - - client.getNotificationSettings = { - Effect.future { callback in - center.getNotificationSettings { settings in - callback(.success(Notification.Settings(rawValue: settings))) - } - }.eraseToEffect() - } - - #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.getNotificationCategories = { - Effect.future { callback in - center.getNotificationCategories { categories in - callback(.success(categories)) - } - } - } - #endif - - client.getPendingNotificationRequests = { - Effect.future { callback in - center.getPendingNotificationRequests { requests in - callback(.success(requests.map(Notification.Request.init(rawValue:)))) - } - } - } - - #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.removeAllDeliveredNotifications = { - .fireAndForget { - center.removeAllDeliveredNotifications() - } - } - #endif - - client.removeAllPendingNotificationRequests = { - .fireAndForget { - center.removeAllPendingNotificationRequests() - } - } - - #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.removeDeliveredNotificationsWithIdentifiers = { identifiers in - .fireAndForget { - center.removeDeliveredNotifications(withIdentifiers: identifiers) - } - } - #endif - - client.removePendingNotificationRequestsWithIdentifiers = { identifiers in - .fireAndForget { - center.removePendingNotificationRequests(withIdentifiers: identifiers) - } - } - - client.requestAuthorization = { options in - .future { callback in - center.requestAuthorization(options: options) { (granted, error) in - if let error = error { - callback(.failure(Error(error))) - } else { - callback(.success(granted)) - } - } - } - } - - #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.setNotificationCategories = { categories in - .fireAndForget { - center.setNotificationCategories(categories) - } - } - #endif - - client.supportsContentExtensions = { - center.supportsContentExtensions - } - - client.delegate = { - Effect.run { subscriber in - var delegate: Optional = Delegate(subscriber: subscriber) - UNUserNotificationCenter.current().delegate = delegate - return AnyCancellable { - delegate = nil - } - } - .share() - .eraseToEffect() - } - - return client - } -} - -private extension UserNotificationClient { - class Delegate: NSObject, UNUserNotificationCenterDelegate { - let subscriber: Effect.Subscriber - - init(subscriber: Effect.Subscriber) { - self.subscriber = subscriber - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - - subscriber.send( - .willPresentNotification( - Notification(rawValue: notification), - completion: completionHandler) - ) - } - - #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - - let wrappedResponse = Notification.Response(rawValue: response) - subscriber.send(.didReceiveResponse(wrappedResponse, completion: completionHandler)) - } - #endif - - #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) - func userNotificationCenter(_ center: UNUserNotificationCenter, - openSettingsFor notification: UNNotification?) { - - - let mappedNotification = notification.map(Notification.init) - subscriber.send(.openSettingsForNotification(mappedNotification)) - } - #endif - } -} diff --git a/Sources/ComposableUserNotifications/LiveKey.swift b/Sources/ComposableUserNotifications/LiveKey.swift new file mode 100644 index 0000000..7ef7744 --- /dev/null +++ b/Sources/ComposableUserNotifications/LiveKey.swift @@ -0,0 +1,126 @@ +import Foundation +import Combine +import ComposableArchitecture +import UserNotifications + +extension UserNotificationClient: DependencyKey { + public static var liveValue: Self { + let center = UNUserNotificationCenter.current() + + var client = UserNotificationClient() + client.add = { try await UNUserNotificationCenter.current().add($0) } + + #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + client.deliveredNotifications = { + let notifications = await center.deliveredNotifications() + return notifications.map(Notification.init(rawValue:)) + } + #endif + + client.notificationSettings = { + let settings = await center.notificationSettings() + return Notification.Settings(rawValue: settings) + } + + #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + client.notificationCategories = { + await center.notificationCategories() + } + #endif + + client.pendingNotificationRequests = { + let requests = await center.pendingNotificationRequests() + return requests.map(Notification.Request.init(rawValue:)) + } + + #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + client.removeAllDeliveredNotifications = { + center.removeAllDeliveredNotifications() + } + #endif + + client.removeAllPendingNotificationRequests = { + center.removeAllPendingNotificationRequests() + } + + #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + client.removeDeliveredNotificationsWithIdentifiers = { identifiers in + center.removeDeliveredNotifications(withIdentifiers: identifiers) + } + #endif + + client.removePendingNotificationRequestsWithIdentifiers = { identifiers in + center.removePendingNotificationRequests(withIdentifiers: identifiers) + } + + client.requestAuthorization = { options in + try await center.requestAuthorization(options: options) + } + + #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + client.setNotificationCategories = { categories in + center.setNotificationCategories(categories) + } + #endif + + client.supportsContentExtensions = { + center.supportsContentExtensions + } + + client.delegate = { + AsyncStream { continuation in + let delegate = Delegate(continuation: continuation) + UNUserNotificationCenter.current().delegate = delegate + continuation.onTermination = { [delegate] _ in } + } + } + + return client + } +} + +private extension UserNotificationClient { + class Delegate: NSObject, UNUserNotificationCenterDelegate { + let continuation: AsyncStream.Continuation + + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + + self.continuation.yield( + .willPresentNotification( + Notification(rawValue: notification), + completionHandler: completionHandler + ) + ) + } + + #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + + let wrappedResponse = Notification.Response(rawValue: response) + self.continuation.yield( + .didReceiveResponse(wrappedResponse) { completionHandler() } + ) + } + #endif + + #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) + func userNotificationCenter(_ center: UNUserNotificationCenter, + openSettingsFor notification: UNNotification?) { + + + let mappedNotification = notification.map(Notification.init) + self.continuation.yield( + .openSettingsForNotification(mappedNotification) + ) + } + #endif + } +} diff --git a/Sources/ComposableUserNotifications/Mock.swift b/Sources/ComposableUserNotifications/Mock.swift deleted file mode 100644 index 9818e66..0000000 --- a/Sources/ComposableUserNotifications/Mock.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import ComposableArchitecture -import UserNotifications - -extension UserNotificationClient { - @available(tvOS, unavailable) - public static func mock( - add: @escaping (UNNotificationRequest) -> Effect = { _ in - _unimplemented("add") - }, - getDeliveredNotifications: @escaping () -> Effect<[Notification], Never> = { - _unimplemented("getDeliveredNotifications") - }, - getNotificationCategories: @escaping () -> Effect, Never> = { - _unimplemented("getNotificationCategories") - }, - getNotificationSettings: @escaping () -> Effect = { - _unimplemented("getNotificationSettings") - }, - getPendingNotificationRequests: @escaping () -> Effect<[Notification.Request], Never> = { - _unimplemented("getPendingNotificationRequests") - }, - removeAllDeliveredNotifications: @escaping () -> Effect = { - _unimplemented("removeAllDeliveredNotifications") - }, - removeAllPendingNotificationRequests: @escaping () -> Effect = { - _unimplemented("removeAllPendingNotificationRequests") - }, - removeDeliveredNotificationsWithIdentifiers: @escaping ([String]) -> Effect = { _ in - _unimplemented("removeDeliveredNotificationsWithIdentifiers") - }, - removePendingNotificationRequestsWithIdentifiers: @escaping ([String]) -> Effect = { _ in - _unimplemented("removePendingNotificationRequestsWithIdentifiers") - }, - requestAuthorization: @escaping (UNAuthorizationOptions) -> Effect = { _ in - _unimplemented("requestAuthorization") - }, - setNotificationCategories: @escaping (Set) -> Effect = { _ in - _unimplemented("setNotificationCategories") - }, - supportsContentExtensions: @escaping () -> Bool = { - _unimplemented("setNotificationCategories") - }, - delegate: @escaping () -> Effect = { - _unimplemented("getDeliveredNotifications") - } - ) -> Self { - Self( - add: add, - getDeliveredNotifications: getDeliveredNotifications, - getNotificationCategories: getNotificationCategories, - getNotificationSettings: getNotificationSettings, - getPendingNotificationRequests: getPendingNotificationRequests, - removeAllDeliveredNotifications: removeAllDeliveredNotifications, - removeAllPendingNotificationRequests: removeAllPendingNotificationRequests, - removeDeliveredNotificationsWithIdentifiers: removeDeliveredNotificationsWithIdentifiers, - removePendingNotificationRequestsWithIdentifiers: removePendingNotificationRequestsWithIdentifiers, - requestAuthorization: requestAuthorization, - setNotificationCategories: setNotificationCategories, - supportsContentExtensions: supportsContentExtensions, - delegate: delegate - ) - } - - @available(iOS, unavailable) - @available(watchOS, unavailable) - @available(macOS, unavailable) - @available(macCatalyst, unavailable) - public static func mock( - add: @escaping (UNNotificationRequest) -> Effect = { _ in - _unimplemented("add") - }, - getNotificationSettings: @escaping () -> Effect = { - _unimplemented("getNotificationSettings") - }, - getPendingNotificationRequests: @escaping () -> Effect<[Notification.Request], Never> = { - _unimplemented("getPendingNotificationRequests") - }, - removeAllPendingNotificationRequests: @escaping () -> Effect = { - _unimplemented("removeAllPendingNotificationRequests") - }, - removePendingNotificationRequestsWithIdentifiers: @escaping ([String]) -> Effect = { _ in - _unimplemented("removePendingNotificationRequestsWithIdentifiers") - }, - requestAuthorization: @escaping (UNAuthorizationOptions) -> Effect = { _ in - _unimplemented("requestAuthorization") - }, - supportsContentExtensions: @escaping () -> Bool = { - _unimplemented("setNotificationCategories") - }, - delegate: @escaping () -> Effect = { - _unimplemented("getDeliveredNotifications") - } - ) -> Self { - Self( - add: add, - getNotificationSettings: getNotificationSettings, - getPendingNotificationRequests: getPendingNotificationRequests, - removeAllPendingNotificationRequests: removeAllPendingNotificationRequests, - removePendingNotificationRequestsWithIdentifiers: removePendingNotificationRequestsWithIdentifiers, - requestAuthorization: requestAuthorization, - supportsContentExtensions: supportsContentExtensions, - delegate: delegate - ) - } -} - -public 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 - ) -} diff --git a/Sources/ComposableUserNotifications/TestKey.swift b/Sources/ComposableUserNotifications/TestKey.swift new file mode 100644 index 0000000..fc5e7e9 --- /dev/null +++ b/Sources/ComposableUserNotifications/TestKey.swift @@ -0,0 +1,77 @@ +import Foundation +import ComposableArchitecture +import UserNotifications +import XCTestDynamicOverlay + +extension DependencyValues { + public var userNotifications: UserNotificationClient { + get { self[UserNotificationClient.self] } + set { self[UserNotificationClient.self] = newValue } + } +} + +extension UserNotificationClient: TestDependencyKey { + public static let previewValue = Self.noop + +#if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) + public static let testValue = Self( + add: XCTUnimplemented("\(Self.self).add"), + deliveredNotifications: XCTUnimplemented("\(Self.self).deliveredNotifications", placeholder: []), + notificationCategories: XCTUnimplemented("\(Self.self).notificationCategories", placeholder: []), + notificationSettings: XCTUnimplemented("\(Self.self).notificationSettings"), + pendingNotificationRequests: XCTUnimplemented("\(Self.self).pendingNotificationRequests"), + removeAllDeliveredNotifications: XCTUnimplemented("\(Self.self).removeAllDeliveredNotifications"), + removeAllPendingNotificationRequests: XCTUnimplemented("\(Self.self).removeAllPendingNotificationRequests"), + removeDeliveredNotificationsWithIdentifiers: XCTUnimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers"), + removePendingNotificationRequestsWithIdentifiers: XCTUnimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers"), + requestAuthorization: XCTUnimplemented("\(Self.self).requestAuthorization"), + setNotificationCategories: XCTUnimplemented("\(Self.self).setNotificationCategories"), + supportsContentExtensions: XCTUnimplemented("\(Self.self).supportsContentExtensions"), + delegate: XCTUnimplemented("\(Self.self).delegate", placeholder: .finished) + ) +#else // tvOS + public static let testValue = Self( + add: XCTUnimplemented("\(Self.self).add"), + deliveredNotifications: XCTUnimplemented("\(Self.self).deliveredNotifications", placeholder: []), + pendingNotificationRequests: XCTUnimplemented("\(Self.self).pendingNotificationRequests"), + removeAllPendingNotificationRequests: XCTUnimplemented("\(Self.self).removeAllPendingNotificationRequests"), + removePendingNotificationRequestsWithIdentifiers: XCTUnimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers"), + requestAuthorization: XCTUnimplemented("\(Self.self).requestAuthorization"), + supportsContentExtensions: XCTUnimplemented("\(Self.self).supportsContentExtensions"), + delegate: XCTUnimplemented("\(Self.self).delegate", placeholder: .finished) + ) +#endif +} + +#if DEBUG +extension UserNotificationClient { + public static let noop = Self( + add: { _ in }, + deliveredNotifications: { [] }, + notificationCategories: { [] }, + notificationSettings: { Notification.Settings(rawValue: .init(coder: NSCoder())!) }, + pendingNotificationRequests: { [] }, + removeAllDeliveredNotifications: { }, + removeAllPendingNotificationRequests: { }, + removeDeliveredNotificationsWithIdentifiers: { _ in }, + removePendingNotificationRequestsWithIdentifiers: { _ in }, + requestAuthorization: { _ in false }, + setNotificationCategories: { _ in }, + supportsContentExtensions: { false }, + delegate: { AsyncStream { _ in } } + ) +} +#endif + +public 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 + ) +} From 9d9f4cc57b06613d1fc6caca403ca04356acee9c Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sat, 5 Nov 2022 17:52:01 +0100 Subject: [PATCH 02/11] Updated tools version. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 407a87f..fe858b8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.6 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 60671b0bfddd9d4494c176ca4cbf77a6828b6d3a Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sat, 5 Nov 2022 17:53:51 +0100 Subject: [PATCH 03/11] Added equatable conformance to Settings. --- Sources/ComposableUserNotifications/Model.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ComposableUserNotifications/Model.swift b/Sources/ComposableUserNotifications/Model.swift index 682fb18..8e5faf4 100644 --- a/Sources/ComposableUserNotifications/Model.swift +++ b/Sources/ComposableUserNotifications/Model.swift @@ -460,7 +460,7 @@ extension Notification.Response { } extension Notification { - public struct Settings { + public struct Settings: Equatable { public var rawValue: () -> UNNotificationSettings? = { _unimplemented("rawValue") } @@ -576,6 +576,10 @@ extension Notification { self.soundSetting = { rawValue.soundSetting } #endif } + + public static func == (lhs: Notification.Settings, rhs: Notification.Settings) -> Bool { + lhs.rawValue() == rhs.rawValue() + } } } From 137aa67c0eee55601ba8295053c57eb5100749e3 Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 18:52:24 +0100 Subject: [PATCH 04/11] Remove explicit setting of preview dependencies. --- Examples/Example/Example/ContentView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Examples/Example/Example/ContentView.swift b/Examples/Example/Example/ContentView.swift index e3b3fa0..ae9de15 100644 --- a/Examples/Example/Example/ContentView.swift +++ b/Examples/Example/Example/ContentView.swift @@ -32,8 +32,6 @@ struct ContentView_Previews: PreviewProvider { store: Store( initialState: .init(), reducer: App() - .dependency(\.userNotifications, .previewValue) - .dependency(\.remote, .previewValue) ) ) } From 20198264d3c79b3b1c7dc8305ac21c717186c3a1 Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 21:29:17 +0100 Subject: [PATCH 05/11] Use renamed unimplemented function. --- Examples/Example/Example/RemoteClient.swift | 2 +- .../Interface.swift | 26 ++-- .../ComposableUserNotifications/Model.swift | 117 ++++++------------ .../ComposableUserNotifications/TestKey.swift | 69 +++++------ 4 files changed, 87 insertions(+), 127 deletions(-) diff --git a/Examples/Example/Example/RemoteClient.swift b/Examples/Example/Example/RemoteClient.swift index 8e9cb52..a587113 100644 --- a/Examples/Example/Example/RemoteClient.swift +++ b/Examples/Example/Example/RemoteClient.swift @@ -32,7 +32,7 @@ extension RemoteClient: TestDependencyKey { ) static let testValue = Self( - fetchRemoteCount: XCTUnimplemented("\(Self.self).fetchRemoteCount") + fetchRemoteCount: unimplemented("\(Self.self).fetchRemoteCount") ) } diff --git a/Sources/ComposableUserNotifications/Interface.swift b/Sources/ComposableUserNotifications/Interface.swift index fc8c731..1fc4e13 100644 --- a/Sources/ComposableUserNotifications/Interface.swift +++ b/Sources/ComposableUserNotifications/Interface.swift @@ -26,40 +26,40 @@ public struct UserNotificationClient { } public var add: @Sendable (UNNotificationRequest) async throws -> Void = - XCTUnimplemented("\(Self.self).add") + unimplemented("\(Self.self).add") @available(tvOS, unavailable) - public var deliveredNotifications: @Sendable () async -> [Notification] = XCTUnimplemented("\(Self.self).deliveredNotifications") + public var deliveredNotifications: @Sendable () async -> [Notification] = unimplemented("\(Self.self).deliveredNotifications") @available(tvOS, unavailable) - public var notificationCategories: () async -> Set = XCTUnimplemented("\(Self.self).deliveredNotifications") + public var notificationCategories: () async -> Set = unimplemented("\(Self.self).deliveredNotifications") - public var notificationSettings: () async -> Notification.Settings = XCTUnimplemented("\(Self.self).notificationSettings") + public var notificationSettings: () async -> Notification.Settings = unimplemented("\(Self.self).notificationSettings") - public var pendingNotificationRequests: () async -> [Notification.Request] = XCTUnimplemented("\(Self.self).pendingNotificationRequests") + public var pendingNotificationRequests: () async -> [Notification.Request] = unimplemented("\(Self.self).pendingNotificationRequests") @available(tvOS, unavailable) - public var removeAllDeliveredNotifications: () async -> Void = XCTUnimplemented("\(Self.self).removeAllDeliveredNotifications") + public var removeAllDeliveredNotifications: () async -> Void = unimplemented("\(Self.self).removeAllDeliveredNotifications") - public var removeAllPendingNotificationRequests: () async -> Void = XCTUnimplemented("\(Self.self).removeAllPendingNotificationRequests") + public var removeAllPendingNotificationRequests: () async -> Void = unimplemented("\(Self.self).removeAllPendingNotificationRequests") @available(tvOS, unavailable) - public var removeDeliveredNotificationsWithIdentifiers: ([String]) async -> Void = XCTUnimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers") + public var removeDeliveredNotificationsWithIdentifiers: ([String]) async -> Void = unimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers") - public var removePendingNotificationRequestsWithIdentifiers: ([String]) async -> Void = XCTUnimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers") + public var removePendingNotificationRequestsWithIdentifiers: ([String]) async -> Void = unimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers") public var requestAuthorization: (UNAuthorizationOptions) async throws -> Bool = - XCTUnimplemented("\(Self.self).requestAuthorization") + unimplemented("\(Self.self).requestAuthorization") @available(tvOS, unavailable) - public var setNotificationCategories: (Set) async -> Void = XCTUnimplemented("\(Self.self).setNotificationCategories") + public var setNotificationCategories: (Set) async -> Void = unimplemented("\(Self.self).setNotificationCategories") - public var supportsContentExtensions: () -> Bool = XCTUnimplemented("\(Self.self).supportsContentExtensions") + public var supportsContentExtensions: () -> Bool = unimplemented("\(Self.self).supportsContentExtensions") /// This Effect represents calls to the `UNUserNotificationCenterDelegate`. /// Handling the completion handlers of the `UNUserNotificationCenterDelegate`s methods /// by multiple observers might lead to unexpected behaviour. - public var delegate: @Sendable () -> AsyncStream = XCTUnimplemented("\(Self.self).delegate", placeholder: .finished) + public var delegate: @Sendable () -> AsyncStream = unimplemented("\(Self.self).delegate", placeholder: .finished) } extension UserNotificationClient.DeletegateAction: Equatable { diff --git a/Sources/ComposableUserNotifications/Model.swift b/Sources/ComposableUserNotifications/Model.swift index 8e5faf4..a4293ec 100644 --- a/Sources/ComposableUserNotifications/Model.swift +++ b/Sources/ComposableUserNotifications/Model.swift @@ -1,5 +1,6 @@ import UserNotifications import CoreLocation +import XCTestDynamicOverlay public struct Notification: Equatable { public let rawValue: UNNotification? @@ -77,70 +78,44 @@ extension Notification { public var rawValue: UNNotificationContent? @available(tvOS, unavailable) - public var title: () -> String = { - _unimplemented("title") - } + public var title: () -> String = unimplemented("title") @available(tvOS, unavailable) - public var subtitle: () -> String = { - _unimplemented("subtitle") - } + public var subtitle: () -> String = unimplemented("subtitle") @available(tvOS, unavailable) - public var body: () -> String = { - _unimplemented("body") - } + public var body: () -> String = unimplemented("body") - public var badge: () -> NSNumber? = { - _unimplemented("badge") - } + public var badge: () -> NSNumber? = unimplemented("badge") @available(tvOS, unavailable) - public var sound: () -> UNNotificationSound? = { - _unimplemented("sound") - } + public var sound: () -> UNNotificationSound? = unimplemented("sound") @available(macOS, unavailable) @available(tvOS, unavailable) - public var launchImageName: () -> String = { - _unimplemented("launchImageName") - } + public var launchImageName: () -> String = unimplemented("launchImageName") @available(tvOS, unavailable) - public var userInfo: () -> [AnyHashable : Any] = { - _unimplemented("userInfo") - } + public var userInfo: () -> [AnyHashable: Any] = unimplemented("userInfo") @available(tvOS, unavailable) - public var attachments: () -> [Notification.Attachment] = { - _unimplemented("attachments") - } + public var attachments: () -> [Notification.Attachment] = unimplemented("attachments") @available(tvOS, unavailable) @available(watchOS, unavailable) - public var summaryArgument: () -> String = { - _unimplemented("summaryArgument") - } + public var summaryArgument: () -> String = unimplemented("summaryArgument") @available(tvOS, unavailable) @available(watchOS, unavailable) - public var summaryArgumentCount: () -> Int = { - _unimplemented("summaryArgumentCount") - } + public var summaryArgumentCount: () -> Int = unimplemented("summaryArgumentCount") @available(tvOS, unavailable) - public var categoryIdentifier: () -> String = { - _unimplemented("categoryIdentifier") - } + public var categoryIdentifier: () -> String = unimplemented("categoryIdentifier") @available(tvOS, unavailable) - public var threadIdentifier: () -> String = { - _unimplemented("threadIdentifier") - } + public var threadIdentifier: () -> String = unimplemented("threadIdentifier") - public var targetContentIdentifier: () -> String? = { - _unimplemented("targetContentIdentifier") - } + public var targetContentIdentifier: () -> String? = unimplemented("targetContentIdentifier") public init(rawValue: UNNotificationContent) { self.rawValue = rawValue @@ -461,74 +436,60 @@ extension Notification.Response { extension Notification { public struct Settings: Equatable { - public var rawValue: () -> UNNotificationSettings? = { - _unimplemented("rawValue") - } + public var rawValue: () -> UNNotificationSettings? = unimplemented("rawValue") @available(tvOS, unavailable) - public var alertSetting: () -> UNNotificationSetting = { - _unimplemented("alertSetting") - } + public var alertSetting: () -> UNNotificationSetting = unimplemented("alertSetting") @available(tvOS, unavailable) @available(watchOS, unavailable) - public var alertStyle: () -> UNAlertStyle = { - _unimplemented("alertStyle") - } + public var alertStyle: () -> UNAlertStyle = unimplemented("alertStyle") @available(macOS, unavailable) @available(tvOS, unavailable) - public var announcementSetting: () -> UNNotificationSetting = { - _unimplemented("announcementSetting") - } + public var announcementSetting: () -> UNNotificationSetting = unimplemented( + "announcementSetting" + ) - public var authorizationStatus: () -> UNAuthorizationStatus = { - _unimplemented("authorizationStatus") - } + public var authorizationStatus: () -> UNAuthorizationStatus = unimplemented( + "authorizationStatus" + ) @available(watchOS, unavailable) - public var badgeSetting: () -> UNNotificationSetting = { - _unimplemented("badgeSetting") - } + public var badgeSetting: () -> UNNotificationSetting = unimplemented("badgeSetting") @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) - public var carPlaySetting: () -> UNNotificationSetting = { - _unimplemented("carPlaySetting") - } + public var carPlaySetting: () -> UNNotificationSetting = unimplemented("carPlaySetting") @available(tvOS, unavailable) - public var criticalAlertSetting: () -> UNNotificationSetting = { - _unimplemented("criticalAlertSetting") - } + public var criticalAlertSetting: () -> UNNotificationSetting = unimplemented( + "criticalAlertSetting" + ) @available(tvOS, unavailable) @available(watchOS, unavailable) - public var lockScreenSetting: () -> UNNotificationSetting = { - _unimplemented("lockScreenSetting") - } + public var lockScreenSetting: () -> UNNotificationSetting = unimplemented("lockScreenSetting") @available(tvOS, unavailable) - public var notificationCenterSetting: () -> UNNotificationSetting = { - _unimplemented("notificationCenterSetting") - } + public var notificationCenterSetting: () -> UNNotificationSetting = unimplemented( + "notificationCenterSetting" + ) @available(tvOS, unavailable) - public var providesAppNotificationSettings: () -> Bool = { - _unimplemented("providesAppNotificationSettings") - } + public var providesAppNotificationSettings: () -> Bool = unimplemented( + "providesAppNotificationSettings" + ) @available(tvOS, unavailable) @available(watchOS, unavailable) - public var showPreviewsSetting: () -> UNShowPreviewsSetting = { - _unimplemented("showPreviewsSetting") - } + public var showPreviewsSetting: () -> UNShowPreviewsSetting = unimplemented( + "showPreviewsSetting" + ) @available(tvOS, unavailable) - public var soundSetting: () -> UNNotificationSetting = { - _unimplemented("soundSetting") - } + public var soundSetting: () -> UNNotificationSetting = unimplemented("soundSetting") public init(rawValue: UNNotificationSettings) { self.rawValue = { rawValue } diff --git a/Sources/ComposableUserNotifications/TestKey.swift b/Sources/ComposableUserNotifications/TestKey.swift index fc5e7e9..785a928 100644 --- a/Sources/ComposableUserNotifications/TestKey.swift +++ b/Sources/ComposableUserNotifications/TestKey.swift @@ -15,30 +15,42 @@ extension UserNotificationClient: TestDependencyKey { #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) public static let testValue = Self( - add: XCTUnimplemented("\(Self.self).add"), - deliveredNotifications: XCTUnimplemented("\(Self.self).deliveredNotifications", placeholder: []), - notificationCategories: XCTUnimplemented("\(Self.self).notificationCategories", placeholder: []), - notificationSettings: XCTUnimplemented("\(Self.self).notificationSettings"), - pendingNotificationRequests: XCTUnimplemented("\(Self.self).pendingNotificationRequests"), - removeAllDeliveredNotifications: XCTUnimplemented("\(Self.self).removeAllDeliveredNotifications"), - removeAllPendingNotificationRequests: XCTUnimplemented("\(Self.self).removeAllPendingNotificationRequests"), - removeDeliveredNotificationsWithIdentifiers: XCTUnimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers"), - removePendingNotificationRequestsWithIdentifiers: XCTUnimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers"), - requestAuthorization: XCTUnimplemented("\(Self.self).requestAuthorization"), - setNotificationCategories: XCTUnimplemented("\(Self.self).setNotificationCategories"), - supportsContentExtensions: XCTUnimplemented("\(Self.self).supportsContentExtensions"), - delegate: XCTUnimplemented("\(Self.self).delegate", placeholder: .finished) + add: unimplemented("\(Self.self).add"), + deliveredNotifications: unimplemented("\(Self.self).deliveredNotifications"), + notificationCategories: unimplemented("\(Self.self).notificationCategories", placeholder: []), + notificationSettings: unimplemented("\(Self.self).notificationSettings"), + pendingNotificationRequests: unimplemented("\(Self.self).pendingNotificationRequests"), + removeAllDeliveredNotifications: unimplemented("\(Self.self).removeAllDeliveredNotifications"), + removeAllPendingNotificationRequests: unimplemented( + "\(Self.self).removeAllPendingNotificationRequests" + ), + removeDeliveredNotificationsWithIdentifiers: unimplemented( + "\(Self.self).removeDeliveredNotificationsWithIdentifiers" + ), + removePendingNotificationRequestsWithIdentifiers: unimplemented( + "\(Self.self).removePendingNotificationRequestsWithIdentifiers" + ), + requestAuthorization: unimplemented("\(Self.self).requestAuthorization", placeholder: false), + setNotificationCategories: unimplemented("\(Self.self).setNotificationCategories"), + supportsContentExtensions: unimplemented( + "\(Self.self).supportsContentExtensions", placeholder: false + ), + delegate: unimplemented("\(Self.self).delegate", placeholder: .finished) ) #else // tvOS public static let testValue = Self( - add: XCTUnimplemented("\(Self.self).add"), - deliveredNotifications: XCTUnimplemented("\(Self.self).deliveredNotifications", placeholder: []), - pendingNotificationRequests: XCTUnimplemented("\(Self.self).pendingNotificationRequests"), - removeAllPendingNotificationRequests: XCTUnimplemented("\(Self.self).removeAllPendingNotificationRequests"), - removePendingNotificationRequestsWithIdentifiers: XCTUnimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers"), - requestAuthorization: XCTUnimplemented("\(Self.self).requestAuthorization"), - supportsContentExtensions: XCTUnimplemented("\(Self.self).supportsContentExtensions"), - delegate: XCTUnimplemented("\(Self.self).delegate", placeholder: .finished) + add: unimplemented("\(Self.self).add"), + deliveredNotifications: unimplemented("\(Self.self).deliveredNotifications"), + pendingNotificationRequests: unimplemented("\(Self.self).pendingNotificationRequests"), + removeAllPendingNotificationRequests: unimplemented( + "\(Self.self).removeAllPendingNotificationRequests" + ), + removePendingNotificationRequestsWithIdentifiers: unimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers"), + requestAuthorization: unimplemented("\(Self.self).requestAuthorization", placeholder: false), + supportsContentExtensions: unimplemented( + "\(Self.self).supportsContentExtensions", placeholder: false + ), + delegate: unimplemented("\(Self.self).delegate", placeholder: .finished) ) #endif } @@ -49,7 +61,7 @@ extension UserNotificationClient { add: { _ in }, deliveredNotifications: { [] }, notificationCategories: { [] }, - notificationSettings: { Notification.Settings(rawValue: .init(coder: NSCoder())!) }, + notificationSettings: unimplemented("\(Self.self).notificationSettings"), pendingNotificationRequests: { [] }, removeAllDeliveredNotifications: { }, removeAllPendingNotificationRequests: { }, @@ -62,16 +74,3 @@ extension UserNotificationClient { ) } #endif - -public 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 - ) -} From 9a5bafbc641701233371ce543270fc380c9bb66a Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 21:33:04 +0100 Subject: [PATCH 06/11] Fixes tvOS noop --- Sources/ComposableUserNotifications/TestKey.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/ComposableUserNotifications/TestKey.swift b/Sources/ComposableUserNotifications/TestKey.swift index 785a928..0172db7 100644 --- a/Sources/ComposableUserNotifications/TestKey.swift +++ b/Sources/ComposableUserNotifications/TestKey.swift @@ -55,8 +55,8 @@ extension UserNotificationClient: TestDependencyKey { #endif } -#if DEBUG extension UserNotificationClient { +#if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) public static let noop = Self( add: { _ in }, deliveredNotifications: { [] }, @@ -72,5 +72,16 @@ extension UserNotificationClient { supportsContentExtensions: { false }, delegate: { AsyncStream { _ in } } ) -} +#else // tvOS + public static let noop = Self( + add: { _ in }, + deliveredNotifications: { [] }, + pendingNotificationRequests: { [] }, + removeAllPendingNotificationRequests: { }, + removePendingNotificationRequestsWithIdentifiers: { _ in }, + requestAuthorization: { _ in false }, + supportsContentExtensions: { false }, + delegate: { AsyncStream { _ in } } + ) #endif +} From b4b7992f1bb884f5113badb5a14ee9a43695f6b1 Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 21:33:59 +0100 Subject: [PATCH 07/11] Removed headers --- Examples/Example/Example/App.swift | 7 ------- Examples/Example/Example/BackgroundNotification.swift | 7 ------- Examples/Example/Example/ContentView.swift | 7 ------- Examples/Example/Example/Helper.swift | 7 ------- Examples/Example/Example/RemoteClient.swift | 7 ------- Examples/Example/Example/SceneDelegate.swift | 7 ------- Examples/Example/Example/UserNotification.swift | 7 ------- Examples/Example/ExampleTests/ExampleTests.swift | 7 ------- 8 files changed, 56 deletions(-) diff --git a/Examples/Example/Example/App.swift b/Examples/Example/Example/App.swift index e71c60a..5c3abbe 100644 --- a/Examples/Example/Example/App.swift +++ b/Examples/Example/Example/App.swift @@ -1,10 +1,3 @@ -// -// AppState.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - import Foundation import ComposableArchitecture import ComposableUserNotifications diff --git a/Examples/Example/Example/BackgroundNotification.swift b/Examples/Example/Example/BackgroundNotification.swift index ea39314..f970786 100644 --- a/Examples/Example/Example/BackgroundNotification.swift +++ b/Examples/Example/Example/BackgroundNotification.swift @@ -1,10 +1,3 @@ -// -// BackgroundNotification.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - import Foundation import UIKit diff --git a/Examples/Example/Example/ContentView.swift b/Examples/Example/Example/ContentView.swift index ae9de15..c83e5b3 100644 --- a/Examples/Example/Example/ContentView.swift +++ b/Examples/Example/Example/ContentView.swift @@ -1,10 +1,3 @@ -// -// ContentView.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - import ComposableArchitecture import ComposableUserNotifications import SwiftUI diff --git a/Examples/Example/Example/Helper.swift b/Examples/Example/Example/Helper.swift index 0c65942..5b72ac5 100644 --- a/Examples/Example/Example/Helper.swift +++ b/Examples/Example/Example/Helper.swift @@ -1,10 +1,3 @@ -// -// Helper.swift -// Example -// -// Created by Michael Kao on 01.11.20. -// - import Foundation struct Unit: Equatable { diff --git a/Examples/Example/Example/RemoteClient.swift b/Examples/Example/Example/RemoteClient.swift index a587113..9321f94 100644 --- a/Examples/Example/Example/RemoteClient.swift +++ b/Examples/Example/Example/RemoteClient.swift @@ -1,10 +1,3 @@ -// -// RemoteClient.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - import Foundation import ComposableArchitecture import XCTestDynamicOverlay diff --git a/Examples/Example/Example/SceneDelegate.swift b/Examples/Example/Example/SceneDelegate.swift index 48d642f..5c2b9a2 100644 --- a/Examples/Example/Example/SceneDelegate.swift +++ b/Examples/Example/Example/SceneDelegate.swift @@ -1,10 +1,3 @@ -// -// ExampleApp.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - import ComposableArchitecture import SwiftUI import UIKit diff --git a/Examples/Example/Example/UserNotification.swift b/Examples/Example/Example/UserNotification.swift index 71d3892..6f1873f 100644 --- a/Examples/Example/Example/UserNotification.swift +++ b/Examples/Example/Example/UserNotification.swift @@ -1,10 +1,3 @@ -// -// UserNotification.swift -// Example -// -// Created by Michael Kao on 31.10.20. -// - import Foundation public enum UserNotification: Equatable { diff --git a/Examples/Example/ExampleTests/ExampleTests.swift b/Examples/Example/ExampleTests/ExampleTests.swift index 8784eaf..b61a529 100644 --- a/Examples/Example/ExampleTests/ExampleTests.swift +++ b/Examples/Example/ExampleTests/ExampleTests.swift @@ -1,10 +1,3 @@ -// -// ExampleTests.swift -// ExampleTests -// -// Created by Michael Kao on 31.10.20. -// - import Combine import ComposableArchitecture import ComposableUserNotifications From f58695fe5e0a2f3337740c3a02e42056f3d792bc Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 21:34:55 +0100 Subject: [PATCH 08/11] Remove unused combine import --- Examples/Example/Example/SceneDelegate.swift | 1 - Examples/Example/ExampleTests/ExampleTests.swift | 1 - Sources/ComposableUserNotifications/LiveKey.swift | 1 - 3 files changed, 3 deletions(-) diff --git a/Examples/Example/Example/SceneDelegate.swift b/Examples/Example/Example/SceneDelegate.swift index 5c2b9a2..4e710db 100644 --- a/Examples/Example/Example/SceneDelegate.swift +++ b/Examples/Example/Example/SceneDelegate.swift @@ -1,7 +1,6 @@ import ComposableArchitecture import SwiftUI import UIKit -import Combine private let store = Store( initialState: App.State(), diff --git a/Examples/Example/ExampleTests/ExampleTests.swift b/Examples/Example/ExampleTests/ExampleTests.swift index b61a529..722d585 100644 --- a/Examples/Example/ExampleTests/ExampleTests.swift +++ b/Examples/Example/ExampleTests/ExampleTests.swift @@ -1,4 +1,3 @@ -import Combine import ComposableArchitecture import ComposableUserNotifications import struct ComposableUserNotifications.Notification diff --git a/Sources/ComposableUserNotifications/LiveKey.swift b/Sources/ComposableUserNotifications/LiveKey.swift index 7ef7744..21b066e 100644 --- a/Sources/ComposableUserNotifications/LiveKey.swift +++ b/Sources/ComposableUserNotifications/LiveKey.swift @@ -1,5 +1,4 @@ import Foundation -import Combine import ComposableArchitecture import UserNotifications From 9b61904d94e1cf1bf7f93c267e6a9771d86b1610 Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 21:41:02 +0100 Subject: [PATCH 09/11] Get rid of unused warning --- Sources/ComposableUserNotifications/LiveKey.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ComposableUserNotifications/LiveKey.swift b/Sources/ComposableUserNotifications/LiveKey.swift index 21b066e..a3fbd8d 100644 --- a/Sources/ComposableUserNotifications/LiveKey.swift +++ b/Sources/ComposableUserNotifications/LiveKey.swift @@ -70,7 +70,9 @@ extension UserNotificationClient: DependencyKey { AsyncStream { continuation in let delegate = Delegate(continuation: continuation) UNUserNotificationCenter.current().delegate = delegate - continuation.onTermination = { [delegate] _ in } + continuation.onTermination = { _ in + let _ = delegate + } } } From 35713d91a89068dc6753424311141b6ecbf3db29 Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Sun, 6 Nov 2022 21:43:41 +0100 Subject: [PATCH 10/11] Simplifies --- .../ComposableUserNotifications/LiveKey.swift | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Sources/ComposableUserNotifications/LiveKey.swift b/Sources/ComposableUserNotifications/LiveKey.swift index a3fbd8d..17fb01d 100644 --- a/Sources/ComposableUserNotifications/LiveKey.swift +++ b/Sources/ComposableUserNotifications/LiveKey.swift @@ -43,22 +43,22 @@ extension UserNotificationClient: DependencyKey { } #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.removeDeliveredNotificationsWithIdentifiers = { identifiers in - center.removeDeliveredNotifications(withIdentifiers: identifiers) + client.removeDeliveredNotificationsWithIdentifiers = { + center.removeDeliveredNotifications(withIdentifiers: $0) } #endif - client.removePendingNotificationRequestsWithIdentifiers = { identifiers in - center.removePendingNotificationRequests(withIdentifiers: identifiers) + client.removePendingNotificationRequestsWithIdentifiers = { + center.removePendingNotificationRequests(withIdentifiers: $0) } - client.requestAuthorization = { options in - try await center.requestAuthorization(options: options) + client.requestAuthorization = { + try await center.requestAuthorization(options: $0) } #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - client.setNotificationCategories = { categories in - center.setNotificationCategories(categories) + client.setNotificationCategories = { + center.setNotificationCategories($0) } #endif @@ -88,11 +88,13 @@ private extension UserNotificationClient { self.continuation = continuation } - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - - self.continuation.yield( + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: + @escaping (UNNotificationPresentationOptions) -> Void + ) { + self.continuation.yield( .willPresentNotification( Notification(rawValue: notification), completionHandler: completionHandler @@ -101,10 +103,11 @@ private extension UserNotificationClient { } #if os(iOS) || os(macOS) || os(watchOS) || targetEnvironment(macCatalyst) - func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { let wrappedResponse = Notification.Response(rawValue: response) self.continuation.yield( .didReceiveResponse(wrappedResponse) { completionHandler() } @@ -113,10 +116,10 @@ private extension UserNotificationClient { #endif #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) - func userNotificationCenter(_ center: UNUserNotificationCenter, - openSettingsFor notification: UNNotification?) { - - + func userNotificationCenter( + _ center: UNUserNotificationCenter, + openSettingsFor notification: UNNotification? + ) { let mappedNotification = notification.map(Notification.init) self.continuation.yield( .openSettingsForNotification(mappedNotification) From 0dd8427153258a79643644402cc2e73cee53353d Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Mon, 7 Nov 2022 18:34:11 +0100 Subject: [PATCH 11/11] Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e51fd4..d22dae6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Composable User Notifications is library that bridges [the Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) and [User Notifications](https://developer.apple.com/documentation/usernotifications). -This library is using the [ReducerProtocol](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/reducerprotocol/) and models it's dependency using [swift concurrency](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/swiftconcurrency) since version 0.3.0. +This library is modelling it's dependency using [swift concurrency](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/swiftconcurrency) since version 0.3.0. * [Example](#example) * [Basic usage](#basic-usage)